Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1a5ef58
Add error messages
jaykim1213 Apr 23, 2026
03cac20
Add error summary message
jaykim1213 Apr 23, 2026
42329cd
Split response error handling logic
jaykim1213 Apr 23, 2026
4bbf44e
Remove duplicate codes and add comments to response errors
jaykim1213 Apr 23, 2026
1826511
Add stimulus validation
jaykim1213 Apr 24, 2026
a1a41c8
Add next question button
jaykim1213 Apr 24, 2026
c413e36
Fix minor things
jaykim1213 Apr 24, 2026
b27d501
Remove component error storing
jaykim1213 Apr 25, 2026
dd94949
Fix response warning to be shown after stimulus error is revealed
jaykim1213 Apr 25, 2026
70fa917
Fix playwright test and bug with the check answer
jaykim1213 Apr 27, 2026
4e0e9b2
Revert configs
jaykim1213 Apr 27, 2026
a308f89
Fix test
jaykim1213 Apr 27, 2026
f1aeb75
Address PR comments
jaykim1213 Apr 27, 2026
27a7724
Fix check answer logic
jaykim1213 May 12, 2026
6ecee50
Merge branch 'dev' into jay/highlightUndone
jaykim1213 May 12, 2026
7d361ee
Remove next question button
jaykim1213 May 27, 2026
78784a4
Merge branch 'dev' into jay/highlightUndone
jaykim1213 May 27, 2026
db1277b
Fix test
jaykim1213 May 27, 2026
4de2f8e
Fix test
jaykim1213 May 27, 2026
1ef0297
Fix iframe controller status always true
jaykim1213 May 28, 2026
726ae06
Merge remote-tracking branch 'origin/dev' into jay/highlightUndone
JackWilb Jun 4, 2026
f25c1c1
Merge remote-tracking branch 'origin/dev' into jay/highlightUndone
JackWilb Jun 5, 2026
bc9a8b5
Fix persisted stimulus validation state
JackWilb Jun 5, 2026
4377489
Merge remote-tracking branch 'origin/dev' into jay/highlightUndone
JackWilb Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions public/demo-form-elements/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,10 @@
],
"minSelections": 1,
"maxSelections": 3,
"default": ["Line", "test"]
"default": [
"Line",
"test"
]
},
{
"id": "default-radio",
Expand Down Expand Up @@ -1003,4 +1006,4 @@
"Sidebar Form Elements"
]
}
}
}
4 changes: 4 additions & 0 deletions src/components/NextButton.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { NextButton } from './NextButton';

const mockNavigate = vi.fn();
const mockGoToNextStep = vi.fn();
const mockOnNext = vi.fn();

let mockIdentifier = 'intro_0';

Expand Down Expand Up @@ -77,6 +78,7 @@ describe('NextButton', () => {
mockIdentifier = 'intro_0';
mockNavigate.mockReset();
mockGoToNextStep.mockReset();
mockOnNext.mockReset();
vi.useFakeTimers();
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement('div');
Expand Down Expand Up @@ -105,6 +107,7 @@ describe('NextButton', () => {
<NextButton
config={config}
checkAnswer={null}
onNext={mockOnNext}
/>,
);
});
Expand All @@ -123,6 +126,7 @@ describe('NextButton', () => {
<NextButton
config={config}
checkAnswer={null}
onNext={mockOnNext}
/>,
);
});
Expand Down
8 changes: 5 additions & 3 deletions src/components/NextButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Props = {
config?: IndividualComponent;
location?: ResponseBlockLocation;
checkAnswer: JSX.Element | null;
onNext: () => void;
};

export function NextButton({
Expand All @@ -29,6 +30,7 @@ export function NextButton({
config,
location,
checkAnswer,
onNext,
}: Props) {
const { isNextDisabled, goToNextStep } = useNextStep();
const studyConfig = useStudyConfig();
Expand Down Expand Up @@ -98,7 +100,7 @@ export function NextButton({
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !disabled && !isNextDisabled && buttonTimerSatisfied) {
goToNextStep();
onNext();
}
};

Expand All @@ -108,7 +110,7 @@ export function NextButton({
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [disabled, isNextDisabled, buttonTimerSatisfied, goToNextStep, nextOnEnter]);
}, [disabled, isNextDisabled, buttonTimerSatisfied, onNext, nextOnEnter]);

const nextButtonDisabled = disabled || isNextDisabled || !buttonTimerSatisfied;
const previousButtonText = config?.previousButtonText ?? studyConfig.uiConfig.previousButtonText ?? 'Previous';
Expand All @@ -126,7 +128,7 @@ export function NextButton({
<Button
type="submit"
disabled={nextButtonDisabled}
onClick={() => goToNextStep()}
onClick={() => onNext()}
px={location === 'sidebar' && checkAnswer ? 8 : undefined}
Comment thread
jaykim1213 marked this conversation as resolved.
>
{label}
Expand Down
7 changes: 3 additions & 4 deletions src/components/response/ButtonsInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
} from '@mantine/core';
import { useMemo } from 'react';
import { ButtonsResponse, ParsedStringOption } from '../../parser/types';
import { generateErrorMessage } from './utils';
import classes from './css/ButtonsInput.module.css';
import { useStoredAnswer } from '../../store/hooks/useStoredAnswer';
import { InputLabel } from './InputLabel';
Expand All @@ -14,12 +13,14 @@ export function ButtonsInput({
response,
disabled,
answer,
error,
index,
enumerateQuestions,
}: {
response: ButtonsResponse;
disabled: boolean;
answer: { value?: string };
error?: string | null;
index: number;
enumerateQuestions: boolean;
}) {
Expand All @@ -39,8 +40,6 @@ export function ButtonsInput({
[optionOrders, options, response.id],
);

const error = useMemo(() => generateErrorMessage(response, answer, orderedOptions), [response, answer, orderedOptions]);

return (
<FocusTrap>
<Radio.Group
Expand All @@ -50,7 +49,7 @@ export function ButtonsInput({
key={response.id}
{...answer}
error={error}
errorProps={{ c: required ? 'red' : 'orange' }}
errorProps={{ c: required ? 'red' : 'orange', fz: 'sm', mt: 'xs' }}
style={{ '--input-description-size': 'calc(var(--mantine-font-size-md) - calc(0.125rem * var(--mantine-scale)))' }}
>
<Flex justify="space-between" align="center" gap="xl" mt="xs">
Expand Down
11 changes: 4 additions & 7 deletions src/components/response/CheckBoxInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
} from '@mantine/core';
import { useEffect, useMemo, useState } from 'react';
import { CheckboxResponse, ParsedStringOption } from '../../parser/types';
import { DONT_KNOW_DEFAULT_VALUE, generateErrorMessage, normalizeCheckboxDontKnowValue } from './utils';
import { DONT_KNOW_DEFAULT_VALUE, normalizeCheckboxDontKnowValue } from './utils';
import { HorizontalHandler } from './HorizontalHandler';
import classes from './css/Checkbox.module.css';
import inputClasses from './css/Input.module.css';
Expand All @@ -16,6 +16,7 @@ export function CheckBoxInput({
response,
disabled,
answer,
error,
index,
enumerateQuestions,
otherValue,
Expand All @@ -24,6 +25,7 @@ export function CheckBoxInput({
response: CheckboxResponse;
disabled: boolean;
answer: { value?: string[]; onChange?: (value: string[]) => void };
error?: string | null;
index: number;
enumerateQuestions: boolean;
otherValue?: object;
Expand All @@ -48,11 +50,6 @@ export function CheckBoxInput({
);

const [otherSelected, setOtherSelected] = useState(false);

const error = useMemo(
() => generateErrorMessage(response, { ...answer, dontKnowChecked: !!dontKnowCheckbox?.checked }, orderedOptions),
[response, answer, orderedOptions, dontKnowCheckbox?.checked],
);
const selectedValues = useMemo(() => (Array.isArray(answer.value) ? answer.value : []), [answer.value]);

useEffect(() => {
Expand Down Expand Up @@ -81,7 +78,7 @@ export function CheckBoxInput({
description={secondaryText}
{...answer}
error={error}
errorProps={{ c: required ? 'red' : 'orange' }}
errorProps={{ c: required ? 'red' : 'orange', fz: 'sm', mt: 'xs' }}
style={{ '--input-description-size': 'calc(var(--mantine-font-size-md) - calc(0.125rem * var(--mantine-scale)))' }}
>
<Box mt="xs">
Expand Down
17 changes: 9 additions & 8 deletions src/components/response/DropdownInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { MultiSelect, Select } from '@mantine/core';
import { DropdownResponse } from '../../parser/types';
import { generateErrorMessage } from './utils';
import classes from './css/Input.module.css';
import { InputLabel } from './InputLabel';
import { OptionLabel } from './OptionLabel';
Expand All @@ -10,12 +9,14 @@ export function DropdownInput({
response,
disabled,
answer,
error,
index,
enumerateQuestions,
}: {
response: DropdownResponse;
disabled: boolean;
answer: { value: string };
error?: string | null;
index: number;
enumerateQuestions: boolean;
}) {
Expand All @@ -40,15 +41,15 @@ export function DropdownInput({
disabled={disabled}
label={prompt.length > 0 && <InputLabel prompt={prompt} required={required} index={index} enumerateQuestions={enumerateQuestions} infoText={infoText} />}
description={secondaryText}
placeholder={answer.value.length === 0 ? placeholder : undefined}
placeholder={!answer.value || answer.value.length === 0 ? placeholder : undefined}
data={optionsAsStringOptions}
radius="md"
size="md"
{...answer}
value={answer.value === '' ? [] : Array.isArray(answer.value) ? answer.value : [answer.value]}
error={generateErrorMessage(response, answer, optionsAsStringOptions)}
value={Array.isArray(answer.value) ? answer.value : answer.value ? [answer.value] : []}
error={error}
withErrorStyles={required}
errorProps={{ c: required ? 'red' : 'orange' }}
errorProps={{ c: required ? 'red' : 'orange', fz: 'sm', mt: 'xs' }}
classNames={{ input: classes.fixDisabled }}
maxDropdownHeight={200}
clearable
Expand All @@ -65,10 +66,10 @@ export function DropdownInput({
radius="md"
size="md"
{...answer}
value={answer.value === '' ? null : answer.value}
error={generateErrorMessage(response, answer, optionsAsStringOptions)}
value={answer.value || null}
error={error}
withErrorStyles={required}
errorProps={{ c: required ? 'red' : 'orange' }}
errorProps={{ c: required ? 'red' : 'orange', fz: 'sm', mt: 'xs' }}
classNames={{ input: classes.fixDisabled }}
maxDropdownHeight={200}
renderOption={renderOption}
Expand Down
5 changes: 4 additions & 1 deletion src/components/response/LikertInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ export function LikertInput({
response,
disabled,
answer,
error,
index,
enumerateQuestions,
}: {
response: LikertResponse;
disabled: boolean;
answer: object;
answer: { value?: string };
error?: string | null;
index: number;
enumerateQuestions: boolean;
}) {
Expand Down Expand Up @@ -38,6 +40,7 @@ export function LikertInput({
disabled={disabled}
response={radioResponse}
answer={answer}
error={error}
index={index}
enumerateQuestions={enumerateQuestions}
stretch
Expand Down
5 changes: 2 additions & 3 deletions src/components/response/MatrixInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import radioClasses from './css/Radio.module.css';
import { useStoredAnswer } from '../../store/hooks/useStoredAnswer';
import { InputLabel } from './InputLabel';
import { OptionLabel } from './OptionLabel';
import { generateErrorMessage } from './utils';
import { parseStringOptions } from '../../utils/stringOptions';
import { getMatrixAnswerOptions, isMatrixDontKnowValue, MATRIX_DONT_KNOW_OPTION } from '../../utils/responseOptions';

Expand Down Expand Up @@ -110,12 +109,14 @@ export function MatrixInput({
answer,
index,
disabled,
error,
enumerateQuestions,
}: {
response: MatrixResponse;
answer: { value?: Record<string, string> };
index: number;
disabled: boolean;
error?: string | null;
enumerateQuestions: boolean;
}) {
const { setMatrixAnswersRadio, setMatrixAnswersCheckbox } = useStoreActions();
Expand Down Expand Up @@ -187,8 +188,6 @@ export function MatrixInput({
dispatchCheckboxUpdate(option.value, isChecked);
};

const error = generateErrorMessage(response, normalizedAnswer);

const _n = _choices.length;
const _m = orderedQuestions.length;
const hasRightQuestionLabels = questions.some((question) => question.rightLabel);
Expand Down
9 changes: 5 additions & 4 deletions src/components/response/NumericInput.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { NumberInput } from '@mantine/core';
import { NumericalResponse } from '../../parser/types';
import { generateErrorMessage } from './utils';
import classes from './css/Input.module.css';
import { InputLabel } from './InputLabel';

export function NumericInput({
response,
disabled,
answer,
error,
index,
enumerateQuestions,
}: {
response: NumericalResponse;
disabled: boolean;
answer: object;
answer: { value?: number };
error?: string | null;
index: number;
enumerateQuestions: boolean;
}) {
Expand All @@ -34,9 +35,9 @@ export function NumericInput({
radius="md"
size="md"
{...answer}
error={generateErrorMessage(response, answer)}
error={error}
withErrorStyles={required}
errorProps={{ c: required ? 'red' : 'orange' }}
errorProps={{ c: required ? 'red' : 'orange', fz: 'sm', mt: 'xs' }}
classNames={{ input: classes.fixDisabled }}
/>
);
Expand Down
9 changes: 4 additions & 5 deletions src/components/response/RadioInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
} from '@mantine/core';
import { useState, useMemo } from 'react';
import { ParsedStringOption, RadioResponse } from '../../parser/types';
import { generateErrorMessage } from './utils';
import { HorizontalHandler } from './HorizontalHandler';
import classes from './css/Radio.module.css';
import inputClasses from './css/Input.module.css';
Expand All @@ -16,14 +15,16 @@ export function RadioInput({
response,
disabled,
answer,
error,
index,
enumerateQuestions,
stretch,
otherValue,
}: {
response: RadioResponse;
disabled: boolean;
answer: object;
answer: { value?: string; onChange?: (value: string) => void };
error?: string | null;
index: number;
enumerateQuestions: boolean;
stretch?: boolean;
Expand Down Expand Up @@ -51,8 +52,6 @@ export function RadioInput({
);

const [otherSelected, setOtherSelected] = useState(false);

const error = useMemo(() => generateErrorMessage(response, answer, orderedOptions), [response, answer, orderedOptions]);
const label = useMemo(() => ((horizontal && labelLocation) ? labelLocation : 'inline'), [labelLocation, horizontal]);

return (
Expand All @@ -63,7 +62,7 @@ export function RadioInput({
key={response.id}
{...answer}
error={error}
errorProps={{ c: required ? 'red' : 'orange' }}
errorProps={{ c: required ? 'red' : 'orange', fz: 'sm', mt: 'xs' }}
style={{ '--input-description-size': 'calc(var(--mantine-font-size-md) - calc(0.125rem * var(--mantine-scale)))' }}
>
{horizontal && label === 'above' && (leftLabel || rightLabel) && (
Expand Down
Loading
Loading