Skip to content

Commit b2eeb85

Browse files
mpflexethangardner
andauthored
Feat: add ability to delete Pattern Option Choice fields (#586)
--------- Co-authored-by: ethangardner <ethan.gardner@gsa.gov>
1 parent 7312054 commit b2eeb85

10 files changed

Lines changed: 3960 additions & 6942 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ NOTES.md
2121
tsconfig.tsbuildinfo
2222
*storybook.log
2323
packages/form-service
24+
25+
# end to end tests
2426
/e2e/test-results/
2527
/e2e/playwright-report/
2628
/e2e/blob-report/

packages/design/.storybook/main.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const config: StorybookConfig = {
1515
addons: [
1616
getAbsolutePath('@storybook/addon-links'),
1717
getAbsolutePath('@storybook/addon-essentials'),
18-
getAbsolutePath('@storybook/addon-interactions'),
1918
getAbsolutePath('@storybook/addon-a11y'),
2019
getAbsolutePath('@storybook/addon-coverage'),
2120
getAbsolutePath('@storybook/experimental-addon-test'),

packages/design/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
"@types/react-dom": "^19.0.4",
5454
"@typescript-eslint/eslint-plugin": "^7.18.0",
5555
"@typescript-eslint/parser": "^7.18.0",
56-
"@uswds/compile": "1.1.0",
56+
"@uswds/compile": "^1.2.2",
5757
"@vitejs/plugin-react": "^4.3.4",
5858
"concurrently": "^8.2.2",
5959
"eslint-plugin-react": "^7.35.0",
@@ -77,7 +77,7 @@
7777
"@tiptap/core": "^2.6.2",
7878
"@tiptap/react": "^2.6.2",
7979
"@tiptap/starter-kit": "^2.6.2",
80-
"@uswds/uswds": "^3.8.1",
80+
"@uswds/uswds": "^3.12.0",
8181
"classnames": "^2.5.1",
8282
"debounce": "^2.1.0",
8383
"deep-equal": "^2.2.3",

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import classnames from 'classnames';
2-
import React, { useState } from 'react';
2+
import React, { useState, useEffect } from 'react';
33

44
import { type RadioGroupProps } from '@gsa-tts/forms-core';
55
import { type RadioGroupPattern } from '@gsa-tts/forms-core';
@@ -8,6 +8,7 @@ import RadioGroup from '../../../../Form/components/RadioGroup/index.js';
88
import { PatternEditComponent } from '../../types.js';
99

1010
import { PatternEditActions } from '../common/PatternEditActions.js';
11+
import { PatternOptionActions } from '../common/PatternOptionActions.js';
1112
import { PatternEditForm } from '../common/PatternEditForm.js';
1213
import { usePatternEditFormContext } from '../common/hooks.js';
1314
import { enLocale as message } from '@gsa-tts/forms-common';
@@ -37,12 +38,27 @@ const RadioGroupPatternEdit: PatternEditComponent<RadioGroupProps> = ({
3738
};
3839

3940
const EditComponent = ({ pattern }: { pattern: RadioGroupPattern }) => {
40-
const { fieldId, getFieldState, register } =
41+
const { fieldId, getFieldState, register, setValue } =
4142
usePatternEditFormContext<RadioGroupPattern>(pattern.id);
42-
const [options, setOptions] = useState(pattern.data.options);
43+
const [options, setOptions] = useState(() => [...pattern.data.options]);
4344
const label = getFieldState('label');
4445
const hint = getFieldState('hint');
4546

47+
const handleOptionLabelChange = (index: number, value: string) => {
48+
const newOptions = [...options];
49+
newOptions[index].label = value;
50+
setOptions(newOptions);
51+
};
52+
53+
const handleDeleteOption = (optionId: string) => {
54+
const newOptions = options.filter(o => o.id !== optionId);
55+
setOptions(newOptions);
56+
};
57+
58+
useEffect(() => {
59+
setValue(`options`, options);
60+
}, [options, setValue]);
61+
4662
return (
4763
<div className="grid-row grid-gap">
4864
<div className="mobile-lg:grid-col-12 margin-bottom-2">
@@ -134,9 +150,14 @@ const EditComponent = ({ pattern }: { pattern: RadioGroupPattern }) => {
134150
className="usa-input bg-primary-lighter"
135151
id={fieldId(`options.${index}.label`)}
136152
{...register(`options.${index}.label`)}
137-
defaultValue={option.label}
153+
value={option.label}
154+
onChange={e => handleOptionLabelChange(index, e.target.value)}
138155
aria-label={`Option ${index + 1} label`}
139156
/>
157+
<PatternOptionActions
158+
optionId={option.id}
159+
onDelete={handleDeleteOption}
160+
/>
140161
</div>
141162
</div>
142163
);
@@ -146,7 +167,7 @@ const EditComponent = ({ pattern }: { pattern: RadioGroupPattern }) => {
146167
type="button"
147168
onClick={event => {
148169
event.preventDefault();
149-
const optionId = `option-${options.length + 1}`;
170+
const optionId = `option-${crypto.randomUUID()}`;
150171
setOptions(
151172
options.concat({
152173
id: optionId,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ const pattern: SelectDropdownPattern = {
1515
label: message.patterns.selectDropdown.displayName,
1616
required: false,
1717
options: [
18-
{ value: 'value1', label: 'Option-1' },
19-
{ value: 'value2', label: 'Option-2' },
20-
{ value: 'value3', label: 'Option-3' },
18+
{ value: 'value1', label: 'Option-1', id: 'option-1' },
19+
{ value: 'value2', label: 'Option-2', id: 'option-2' },
20+
{ value: 'value3', label: 'Option-3', id: 'option-3' },
2121
],
2222
},
2323
};

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

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import classnames from 'classnames';
2-
import React, { useState } from 'react';
2+
import React, { useState, useEffect } from 'react';
33

44
import { type SelectDropdownProps } from '@gsa-tts/forms-core';
55
import { type SelectDropdownPattern } from '@gsa-tts/forms-core';
@@ -8,6 +8,7 @@ import SelectDropdown from '../../../../Form/components/SelectDropdown/index.js'
88
import { PatternEditComponent } from '../../types.js';
99

1010
import { PatternEditActions } from '../common/PatternEditActions.js';
11+
import { PatternOptionActions } from '../common/PatternOptionActions.js';
1112
import { PatternEditForm } from '../common/PatternEditForm.js';
1213
import { usePatternEditFormContext } from '../common/hooks.js';
1314
import { enLocale as message } from '@gsa-tts/forms-common';
@@ -38,12 +39,27 @@ const SelectDropdownPatternEdit: PatternEditComponent<SelectDropdownProps> = ({
3839
};
3940

4041
const EditComponent = ({ pattern }: { pattern: SelectDropdownPattern }) => {
41-
const { fieldId, getFieldState, register } =
42+
const { fieldId, getFieldState, register, setValue } =
4243
usePatternEditFormContext<SelectDropdownPattern>(pattern.id);
4344
const [options, setOptions] = useState(pattern.data.options);
4445
const label = getFieldState('label');
4546
const hint = getFieldState('hint');
4647

48+
const handleOptionLabelChange = (index: number, value: string) => {
49+
const newOptions = [...options];
50+
newOptions[index].label = value;
51+
setOptions(newOptions);
52+
};
53+
54+
const handleDeleteOption = (optionId: string) => {
55+
const newOptions = options.filter(o => o.id !== optionId);
56+
setOptions(newOptions);
57+
};
58+
59+
useEffect(() => {
60+
setValue(`options`, options);
61+
}, [options, setValue]);
62+
4763
return (
4864
<div className="grid-row grid-gap">
4965
<div className="mobile-lg:grid-col-12 margin-bottom-2">
@@ -133,22 +149,32 @@ const EditComponent = ({ pattern }: { pattern: SelectDropdownPattern }) => {
133149
className="usa-input bg-primary-lighter"
134150
id={fieldId(`options.${index}.label`)}
135151
{...register(`options.${index}.label`)}
136-
defaultValue={option.label}
152+
value={option.label}
153+
onChange={e => handleOptionLabelChange(index, e.target.value)}
137154
aria-label={`Option ${index + 1} label`}
138155
/>
156+
<PatternOptionActions
157+
optionId={option.id}
158+
onDelete={handleDeleteOption}
159+
/>
139160
</div>
140161
</div>
141162
);
142163
})}
143164
<button
144-
className="usa-button usa-button--outline margin-top-1"
165+
className={`usa-link ${styles.addMorePatternButton} margin-top-1`}
145166
type="button"
146167
onClick={event => {
147168
event.preventDefault();
148169
const optionLabel = `Option ${options.length + 1}`;
149170
const optionValue = `value-${options.length + 1}`;
171+
const optionId = `option-${crypto.randomUUID()}`;
150172
setOptions(
151-
options.concat({ value: optionValue, label: optionLabel })
173+
options.concat({
174+
value: optionValue,
175+
label: optionLabel,
176+
id: optionId,
177+
})
152178
);
153179
}}
154180
>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
import { useFormManagerStore } from '../../../store.js';
3+
4+
type PatternOptionActionProps = {
5+
optionId: string;
6+
onDelete: (optionId: string) => void;
7+
ariaLabel?: string;
8+
title?: string;
9+
};
10+
11+
export const PatternOptionActions: React.FC<PatternOptionActionProps> = ({
12+
optionId,
13+
onDelete,
14+
ariaLabel = 'Delete this option',
15+
title = 'Delete this option',
16+
}) => {
17+
const { uswdsRoot } = useFormManagerStore(state => ({
18+
uswdsRoot: state.context.uswdsRoot,
19+
}));
20+
21+
return (
22+
<button
23+
type="button"
24+
aria-label={ariaLabel}
25+
title={title}
26+
className="usa-button--outline usa-button--unstyled"
27+
onClick={event => {
28+
event.preventDefault();
29+
const confirmed = window.confirm(
30+
'Are you sure you want to delete this option?'
31+
);
32+
if (confirmed) {
33+
onDelete(optionId);
34+
}
35+
}}
36+
>
37+
<svg
38+
className="usa-icon usa-icon--size-3 margin-1 text-middle"
39+
aria-hidden="true"
40+
focusable="false"
41+
role="img"
42+
>
43+
<use xlinkHref={`${uswdsRoot}img/sprite.svg#delete`}></use>
44+
</svg>
45+
</button>
46+
);
47+
};
48+
49+
export default PatternOptionActions;

packages/forms/src/patterns/select-dropdown/select-dropdown.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ describe('SelectDropdownPattern tests', () => {
1212
label: 'Test Label',
1313
required: true,
1414
options: [
15-
{ value: 'value1', label: 'Option 1' },
16-
{ value: 'value2', label: 'Option 2' },
15+
{ value: 'value1', label: 'Option 1', id: 'option-1' },
16+
{ value: 'value2', label: 'Option 2', id: 'option-2' },
1717
],
1818
};
1919

@@ -30,8 +30,8 @@ describe('SelectDropdownPattern tests', () => {
3030
label: 'Test Label',
3131
required: false,
3232
options: [
33-
{ value: 'value1', label: 'Option 1' },
34-
{ value: 'value2', label: 'Option 2' },
33+
{ value: 'value1', label: 'Option 1', id: 'option-1' },
34+
{ value: 'value2', label: 'Option 2', id: 'option-2' },
3535
],
3636
};
3737

@@ -64,8 +64,8 @@ describe('SelectDropdownPattern tests', () => {
6464
label: 'Test Dropdown',
6565
required: true,
6666
options: [
67-
{ value: 'value1', label: 'Option 1' },
68-
{ value: 'value2', label: 'Option 2' },
67+
{ value: 'value1', label: 'Option 1', id: 'option-1' },
68+
{ value: 'value2', label: 'Option 2', id: 'option-2' },
6969
],
7070
},
7171
};
@@ -90,8 +90,8 @@ describe('SelectDropdownPattern tests', () => {
9090
label: 'Test Dropdown',
9191
required: true,
9292
options: [
93-
{ value: 'value1', label: 'Option 1' },
94-
{ value: 'value2', label: 'Option 2' },
93+
{ value: 'value1', label: 'Option 1', id: 'option-1' },
94+
{ value: 'value2', label: 'Option 2', id: 'option-2' },
9595
],
9696
},
9797
};
@@ -116,8 +116,8 @@ describe('SelectDropdownPattern tests', () => {
116116
label: 'Test Dropdown',
117117
required: true,
118118
options: [
119-
{ value: 'value1', label: 'Option 1' },
120-
{ value: 'value2', label: 'Option 2' },
119+
{ value: 'value1', label: 'Option 1', id: 'option-1' },
120+
{ value: 'value2', label: 'Option 2', id: 'option-2' },
121121
],
122122
};
123123

packages/forms/src/patterns/select-dropdown/select-dropdown.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const configSchema = z.object({
1717
value: z
1818
.string()
1919
.regex(/^[A-Za-z][A-Za-z0-9\-_:.]*$/, 'Invalid Option Value'),
20+
id: z.string().regex(/^[A-Za-z][A-Za-z0-9\-_:.]*$/, 'Invalid Option ID'),
2021
label: z.string().min(1),
2122
})
2223
.array(),
@@ -68,9 +69,9 @@ export const selectDropdownConfig: PatternConfig<
6869
label: 'Dropdown-label',
6970
required: false,
7071
options: [
71-
{ value: 'value1', label: 'Option 1' },
72-
{ value: 'value2', label: 'Option 2' },
73-
{ value: 'value3', label: 'Option 3' },
72+
{ value: 'value1', label: 'Option 1', id: 'option-1' },
73+
{ value: 'value2', label: 'Option 2', id: 'option-2' },
74+
{ value: 'value3', label: 'Option 3', id: 'option-3' },
7475
],
7576
},
7677

@@ -104,6 +105,7 @@ export const selectDropdownConfig: PatternConfig<
104105
return {
105106
value: option.value,
106107
label: option.label,
108+
id: option.id,
107109
};
108110
}),
109111
required: pattern.data.required,

0 commit comments

Comments
 (0)