From 04246e2edb6c32d3bbe00cf514b87bab741fdc0e Mon Sep 17 00:00:00 2001 From: Mateusz Prusaczyk Date: Mon, 19 Oct 2026 13:42:44 +0200 Subject: [PATCH] feat(feedback-module): multiple choice options in the survey question card Co-authored-by: Claude Opus 5.5 --- .../ChoiceOptions/ChoiceOptions.tsx | 98 ++++++++++ .../QuestionCard/QuestionCard.tsx | 172 ++++++++++-------- .../SurveyQuestions/SurveyQuestions.test.tsx | 73 ++++++++ 3 files changed, 266 insertions(+), 77 deletions(-) create mode 100644 frontend/src/component/feedbackModule/CreateSurvey/SurveyQuestions/QuestionCard/ChoiceOptions/ChoiceOptions.tsx 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' + > + + + + ), + }, + }} + /> +
  • + ); + })} +
    + +
    +); 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(