diff --git a/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/QuestionCard/ChoiceOptions/ChoiceOptions.tsx b/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/QuestionCard/ChoiceOptions/ChoiceOptions.tsx
new file mode 100644
index 0000000000..2797d0964a
--- /dev/null
+++ b/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/QuestionCard/ChoiceOptions/ChoiceOptions.tsx
@@ -0,0 +1,98 @@
+import {
+ Button,
+ IconButton,
+ InputAdornment,
+ styled,
+ TextField,
+} from '@mui/material';
+import Add from '@mui/icons-material/Add';
+import CheckBoxOutlineBlank from '@mui/icons-material/CheckBoxOutlineBlank';
+import Close from '@mui/icons-material/Close';
+import { FormGroup } from 'component/common/FormGroup/FormGroup';
+
+const StyledOptionList = styled('ul')(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(1),
+ margin: 0,
+ padding: 0,
+ listStyle: 'none',
+}));
+
+interface ChoiceOptionsProps {
+ options: string[];
+ errors?: (string | undefined)[];
+ onChange: (options: string[]) => void;
+}
+
+export const ChoiceOptions = ({
+ options,
+ errors = [],
+ onChange,
+}: ChoiceOptionsProps) => (
+
+
+ {options.map((option, index) => {
+ const label = `Option ${index + 1}`;
+ return (
+ // Options are plain strings that can be empty or repeated
+ // while typed, so they have nothing stable to key by.
+
+
+ onChange(
+ options.with(index, event.target.value),
+ )
+ }
+ size='small'
+ fullWidth
+ error={Boolean(errors[index])}
+ helperText={errors[index]}
+ slotProps={{
+ htmlInput: { 'aria-label': label },
+ input: {
+ startAdornment: (
+
+
+
+ ),
+ endAdornment: (
+
+
+ onChange(
+ options.toSpliced(
+ index,
+ 1,
+ ),
+ )
+ }
+ disabled={options.length === 1}
+ edge='end'
+ size='small'
+ >
+
+
+
+ ),
+ },
+ }}
+ />
+
+ );
+ })}
+
+ }
+ onClick={() => onChange([...options, ''])}
+ >
+ Add option
+
+
+);
diff --git a/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/QuestionCard/QuestionCard.tsx b/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/QuestionCard/QuestionCard.tsx
index 23a0be8a53..b83ef79f5b 100644
--- a/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/QuestionCard/QuestionCard.tsx
+++ b/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/QuestionCard/QuestionCard.tsx
@@ -19,9 +19,11 @@ import {
type QuestionType,
withType,
} from '../../surveyDraft.ts';
+import { ChoiceOptions } from './ChoiceOptions/ChoiceOptions.tsx';
const QUESTION_TYPES: { key: QuestionType; label: string }[] = [
{ key: 'rating', label: 'Rating' },
+ { key: 'multi-choice', label: 'Multiple choice' },
{ key: 'free-text', label: 'Free text' },
];
@@ -33,7 +35,7 @@ const SCALES = Array.from(
},
);
-const StyledCard = styled('li')(({ theme }) => ({
+const StyledCard = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
@@ -87,85 +89,101 @@ export const QuestionCard = ({
) => onChange({ ...question, ...fields });
return (
-
-
- update({ prompt: event.target.value })}
- error={Boolean(errors.prompt)}
- errorText={errors.prompt}
- fullWidth
- />
- onChange(withType(question, type))}
- options={QUESTION_TYPES}
- />
-
- ID
-
- >
- }
- value={question.id}
- onChange={(event) => update({ id: event.target.value })}
- error={Boolean(errors.id)}
- errorText={errors.id}
- fullWidth
- />
-
- {question.type === 'rating' ? (
-
-
- onChange({ ...question, scale: Number(scale) })
+
+
+
+
+ update({ prompt: event.target.value })
}
- options={SCALES}
+ error={Boolean(errors.prompt)}
+ errorText={errors.prompt}
+ fullWidth
/>
-
- ) : null}
-
-
-
-
-
-
-
-
-
-
- update({ required: event.target.checked })
+ onChange(withType(question, type))}
+ options={QUESTION_TYPES}
+ />
+
+ ID
+
+ >
+ }
+ value={question.id}
+ onChange={(event) => update({ id: event.target.value })}
+ error={Boolean(errors.id)}
+ errorText={errors.id}
+ fullWidth
+ />
+
+ {question.type === 'rating' ? (
+
+
+ onChange({ ...question, scale: Number(scale) })
}
+ options={SCALES}
/>
- }
- />
-
-
+
+ ) : null}
+ {question.type === 'multi-choice' ? (
+
+ onChange({ ...question, options })
+ }
+ />
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+ update({ required: event.target.checked })
+ }
+ />
+ }
+ />
+
+
+
);
};
diff --git a/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/SurveyQuestions.test.tsx b/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/SurveyQuestions.test.tsx
index e16979e58e..e2e668e6ff 100644
--- a/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/SurveyQuestions.test.tsx
+++ b/frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/SurveyQuestions.test.tsx
@@ -18,6 +18,12 @@ const draftQuestions = (count: number): QuestionDraft[] =>
prompt: `Prompt ${index + 1}`,
}));
+const choiceQuestion = (options: string[]): QuestionDraft => ({
+ ...draftQuestions(1)[0],
+ type: 'multi-choice',
+ options,
+});
+
const Editable = ({
initial,
errors = [],
@@ -135,6 +141,73 @@ describe('SurveyQuestions', () => {
).toHaveTextContent('7 stars');
});
+ it('asks for options only for multiple choice questions', async () => {
+ render();
+ const card = questionCard(1);
+
+ await userEvent.click(
+ within(card).getByRole('combobox', { name: 'Type' }),
+ );
+ await userEvent.click(
+ screen.getByRole('option', { name: 'Multiple choice' }),
+ );
+
+ expect(within(card).getByLabelText('Option 1')).toHaveValue('');
+ expect(
+ within(card).queryByRole('combobox', { name: 'Scale' }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('keeps the options entered into a multiple choice question', async () => {
+ render();
+ const card = questionCard(1);
+
+ await userEvent.click(
+ within(card).getByRole('button', { name: 'Add option' }),
+ );
+ await userEvent.type(within(card).getByLabelText('Option 2'), 'PM');
+
+ expect(within(card).getByLabelText('Option 1')).toHaveValue('Dev');
+ expect(within(card).getByLabelText('Option 2')).toHaveValue('PM');
+ });
+
+ it('removes the chosen option', async () => {
+ render();
+ const card = questionCard(1);
+
+ await userEvent.click(
+ within(card).getByRole('button', { name: 'Remove option 1' }),
+ );
+
+ expect(within(card).getByLabelText('Option 1')).toHaveValue('PM');
+ expect(
+ within(card).queryByLabelText('Option 2'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('keeps the last option', () => {
+ render();
+
+ expect(
+ within(questionCard(1)).getByRole('button', {
+ name: 'Remove option 1',
+ }),
+ ).toBeDisabled();
+ });
+
+ it('shows the errors of each option', () => {
+ render(
+ ,
+ );
+
+ expect(
+ within(questionCard(1)).getByLabelText('Option 2'),
+ ).toHaveAccessibleDescription('Enter an option');
+ });
+
it('shows the errors of each question', () => {
render(