From c5c666c178be2317dfc4714983ea726e8c8b8181 Mon Sep 17 00:00:00 2001 From: Maryam Mehdizadeh Date: Mon, 20 Jul 2026 09:54:11 +0800 Subject: [PATCH] feat(#1984): surface validation feedback severity in field components Add useValidationFeedbackSeverity hook returning { feedback, feedbackSeverity }. Warning-level targetConstraint violations and OperationOutcome issues now render with amber helper text and no red border, while error-level feedback continues to use the existing red styling. useValidationFeedback is kept as a deprecated thin wrapper for backward compatibility. Co-authored-by: Cursor --- .../AttachmentItem/AttachmentField.tsx | 9 +- .../AttachmentItem/AttachmentFieldWrapper.tsx | 4 + .../AttachmentItem/AttachmentItem.tsx | 5 +- .../BooleanItem/BooleanField.tsx | 9 +- .../BooleanItem/BooleanItem.tsx | 7 +- .../ChoiceItems/ChoiceAutocompleteItem.tsx | 7 +- .../ChoiceCheckboxAnswerOptionFields.tsx | 3 + .../ChoiceCheckboxAnswerOptionItem.tsx | 6 +- .../ChoiceCheckboxAnswerValueSetFields.tsx | 3 + .../ChoiceCheckboxAnswerValueSetItem.tsx | 6 +- .../ChoiceRadioAnswerOptionFields.tsx | 3 + .../ChoiceRadioAnswerOptionItem.tsx | 6 +- .../ChoiceRadioAnswerOptionView.tsx | 4 + .../ChoiceRadioAnswerValueSetFields.tsx | 3 + .../ChoiceRadioAnswerValueSetItem.tsx | 6 +- .../ChoiceSelectAnswerOptionFields.tsx | 8 +- .../ChoiceSelectAnswerOptionItem.tsx | 5 +- .../ChoiceSelectAnswerOptionView.tsx | 4 + .../ChoiceSelectAnswerValueSetFields.tsx | 9 +- .../ChoiceSelectAnswerValueSetItem.tsx | 6 +- .../CustomDateItem/CustomDateField.tsx | 7 +- .../CustomDateItem/CustomDateItem.tsx | 9 +- .../CustomDateTimeItem/CustomDateTimeItem.tsx | 9 +- .../CustomDateTimeItem/DateTimeField.tsx | 3 + .../DecimalItem/DecimalField.tsx | 7 +- .../DecimalItem/DecimalItem.tsx | 6 +- .../IntegerItem/IntegerField.tsx | 7 +- .../IntegerItem/IntegerItem.tsx | 6 +- .../ItemParts/CheckboxFormGroup.tsx | 9 +- .../ItemParts/RadioFormGroup.tsx | 9 +- .../OpenChoiceAutocompleteItem.tsx | 7 +- .../OpenChoiceCheckboxAnswerOptionFields.tsx | 3 + .../OpenChoiceCheckboxAnswerOptionItem.tsx | 6 +- ...OpenChoiceCheckboxAnswerValueSetFields.tsx | 3 + .../OpenChoiceCheckboxAnswerValueSetItem.tsx | 6 +- .../OpenChoiceRadioAnswerOptionFields.tsx | 3 + .../OpenChoiceRadioAnswerOptionItem.tsx | 5 +- .../OpenChoiceRadioAnswerValueSetFields.tsx | 3 + .../OpenChoiceRadioAnswerValueSetItem.tsx | 5 +- .../OpenChoiceSelectAnswerOptionField.tsx | 8 +- .../OpenChoiceSelectAnswerOptionItem.tsx | 6 +- .../OpenChoiceSelectAnswerValueSetField.tsx | 8 +- .../OpenChoiceSelectAnswerValueSetItem.tsx | 6 +- .../QuantityItem/QuantityField.tsx | 7 +- .../QuantityItem/QuantityItem.tsx | 6 +- .../FormComponents/SliderItem/SliderField.tsx | 9 +- .../FormComponents/SliderItem/SliderItem.tsx | 6 +- .../FormComponents/StringItem/StringField.tsx | 7 +- .../FormComponents/StringItem/StringItem.tsx | 6 +- .../FormComponents/TextItem/TextField.tsx | 7 +- .../FormComponents/TextItem/TextItem.tsx | 6 +- .../FormComponents/UrlItem/UrlField.tsx | 7 +- .../FormComponents/UrlItem/UrlItem.tsx | 6 +- .../src/hooks/useValidationFeedback.ts | 184 +----------- .../hooks/useValidationFeedbackSeverity.ts | 247 ++++++++++++++++ .../useValidationFeedbackSeverity.test.ts | 268 ++++++++++++++++++ 56 files changed, 768 insertions(+), 252 deletions(-) create mode 100644 packages/smart-forms-renderer/src/hooks/useValidationFeedbackSeverity.ts create mode 100644 packages/smart-forms-renderer/src/test/useValidationFeedbackSeverity.test.ts diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx index 9ed583873..dde7c199e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx @@ -35,6 +35,7 @@ interface AttachmentFieldProps extends PropsWithIsTabledAttribute { itemText: string | undefined; attachmentValues: AttachmentValues; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; instructionsId: string | undefined; onUploadFile: (file: File | null) => void; @@ -49,6 +50,7 @@ function AttachmentField(props: AttachmentFieldProps) { itemText, attachmentValues, feedback, + feedbackSeverity, readOnly, isTabled, instructionsId, @@ -148,7 +150,12 @@ function AttachmentField(props: AttachmentFieldProps) { - {feedback ? {feedback} : null} + {feedback ? ( + + {feedback} + + ) : null} ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx index 9750aae77..5e1f28332 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx @@ -34,6 +34,7 @@ interface AttachmentFieldWrapperProps qItem: QuestionnaireItem; attachmentValues: AttachmentValues; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; instructionsId: string | undefined; onUploadFile: (file: File | null) => void; @@ -46,6 +47,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) { qItem, attachmentValues, feedback, + feedbackSeverity, readOnly, isRepeated, isTabled, @@ -65,6 +67,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) { itemText={qItem.text} attachmentValues={attachmentValues} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} isTabled={isTabled} instructionsId={instructionsId} @@ -92,6 +95,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) { itemText={qItem.text} attachmentValues={attachmentValues} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} isTabled={isTabled} instructionsId={instructionsId} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentItem.tsx index 34e4086b2..53d90e059 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentItem.tsx @@ -26,7 +26,7 @@ import AttachmentFieldWrapper from './AttachmentFieldWrapper'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { DndProvider } from 'react-dnd'; import { createAttachmentAnswer } from '../../../utils/fileUtils'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import { getInstructionsId } from '../ItemParts/ItemFieldGrid'; export interface AttachmentValues { @@ -60,7 +60,7 @@ function AttachmentItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); // Get instructions ID for aria-describedby const { displayInstructions } = useRenderingExtensions(qItem); @@ -114,6 +114,7 @@ function AttachmentItem(props: BaseItemProps) { qItem={qItem} attachmentValues={{ uploadedFile: uploadedFile, url: url, fileName: fileName }} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} isRepeated={isRepeated} isTabled={isTabled} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx index 053bfa821..d3e7eb509 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx @@ -37,6 +37,7 @@ interface BooleanFieldProps { readOnly: boolean; valueBoolean: boolean | undefined; feedback: string; + feedbackSeverity?: 'error' | 'warning'; calcExpUpdated: boolean; instructionsId: string | undefined; onCheckedChange: (newValue: string) => void; @@ -49,6 +50,7 @@ const BooleanField = memo(function BooleanField(props: BooleanFieldProps) { readOnly, valueBoolean, feedback, + feedbackSeverity, calcExpUpdated, instructionsId, onCheckedChange, @@ -182,7 +184,12 @@ const BooleanField = memo(function BooleanField(props: BooleanFieldProps) { /> - {feedback ? {feedback} : null} + {feedback ? ( + + {feedback} + + ) : null} ); }); diff --git a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx index 19212fe7b..28956440e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx @@ -17,7 +17,7 @@ import Box from '@mui/material/Box'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; import { createEmptyQrItem, getQRItemId } from '../../../utils/qrItem'; @@ -44,7 +44,7 @@ function BooleanItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); // Get instructions ID for aria-describedby const { displayInstructions } = useRenderingExtensions(qItem); @@ -90,6 +90,7 @@ function BooleanItem(props: BaseItemProps) { readOnly={readOnly} valueBoolean={valueBoolean} feedback={feedback} + feedbackSeverity={feedbackSeverity} calcExpUpdated={calcExpUpdated} instructionsId={instructionsId} onCheckedChange={handleValueChange} @@ -106,6 +107,7 @@ function BooleanItem(props: BaseItemProps) { readOnly={readOnly} valueBoolean={valueBoolean} feedback={feedback} + feedbackSeverity={feedbackSeverity} calcExpUpdated={calcExpUpdated} instructionsId={instructionsId} onCheckedChange={handleValueChange} @@ -130,6 +132,7 @@ function BooleanItem(props: BaseItemProps) { readOnly={readOnly} valueBoolean={valueBoolean} feedback={feedback} + feedbackSeverity={feedbackSeverity} calcExpUpdated={calcExpUpdated} instructionsId={instructionsId} onCheckedChange={handleValueChange} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx index 571fbe5c8..2ab2d07ea 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceAutocompleteItem.tsx @@ -22,7 +22,7 @@ import type { AlertColor } from '@mui/material/Alert'; import useDebounce from '../../../hooks/useDebounce'; import useReadOnly from '../../../hooks/useReadOnly'; import useTerminologyServerQuery from '../../../hooks/useTerminologyServerQuery'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; import { AUTOCOMPLETE_DEBOUNCE_DURATION } from '../../../utils/debounce'; @@ -64,7 +64,8 @@ function ChoiceAutocompleteItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const validationFeedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback: validationFeedback, feedbackSeverity: validationFeedbackSeverity } = + useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = renderingExtensions; const instructionsId = displayInstructions && !validationFeedback ? `instructions-${qItem.linkId}` : undefined; @@ -79,7 +80,7 @@ function ChoiceAutocompleteItem(props: BaseItemProps) { if (terminologyFeedback) { feedback = terminologyFeedback; } else if (validationFeedback !== '') { - feedback = { message: validationFeedback, color: 'error' }; + feedback = { message: validationFeedback, color: validationFeedbackSeverity }; } if (!qItem.answerValueSet) { diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx index abbd91a68..f0c8a8fd8 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx @@ -27,6 +27,7 @@ interface ChoiceCheckboxAnswerOptionFieldsProps { options: QuestionnaireItemAnswerOption[]; answers: QuestionnaireResponseItemAnswer[]; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -42,6 +43,7 @@ function ChoiceCheckboxAnswerOptionFields(props: ChoiceCheckboxAnswerOptionField options, answers, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -57,6 +59,7 @@ function ChoiceCheckboxAnswerOptionFields(props: ChoiceCheckboxAnswerOptionField options={options} answers={answers} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx index 65b551ac7..e6d760b24 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx @@ -17,7 +17,7 @@ import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; import { updateChoiceCheckboxAnswers } from '../../../utils/choice'; @@ -57,7 +57,7 @@ function ChoiceCheckboxAnswerOptionItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback); const options = qItem.answerOption ?? []; @@ -95,6 +95,7 @@ function ChoiceCheckboxAnswerOptionItem(props: BaseItemProps) { options={options} answers={answers} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} @@ -124,6 +125,7 @@ function ChoiceCheckboxAnswerOptionItem(props: BaseItemProps) { options={options} answers={answers} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx index c248b5050..3ea6eb3f7 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx @@ -31,6 +31,7 @@ interface ChoiceCheckboxAnswerValueSetFieldsProps { options: QuestionnaireItemAnswerOption[]; answers: QuestionnaireResponseItemAnswer[]; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -47,6 +48,7 @@ function ChoiceCheckboxAnswerValueSetFields(props: ChoiceCheckboxAnswerValueSetF options, answers, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -64,6 +66,7 @@ function ChoiceCheckboxAnswerValueSetFields(props: ChoiceCheckboxAnswerValueSetF options={options} answers={answers} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx index 1d6927932..d882ed8c9 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx @@ -18,7 +18,7 @@ import { useMemo } from 'react'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -55,7 +55,7 @@ function ChoiceCheckboxAnswerValueSetItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback); // Get codings/options from valueSet @@ -101,6 +101,7 @@ function ChoiceCheckboxAnswerValueSetItem(props: BaseItemProps) { options={options} answers={answers} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} @@ -131,6 +132,7 @@ function ChoiceCheckboxAnswerValueSetItem(props: BaseItemProps) { options={options} answers={answers} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx index 60286cc62..5a779a61b 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx @@ -23,6 +23,7 @@ interface ChoiceRadioAnswerOptionFieldsProps { options: QuestionnaireItemAnswerOption[]; valueRadio: string | null; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -38,6 +39,7 @@ function ChoiceRadioAnswerOptionFields(props: ChoiceRadioAnswerOptionFieldsProps options, valueRadio, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -53,6 +55,7 @@ function ChoiceRadioAnswerOptionFields(props: ChoiceRadioAnswerOptionFieldsProps options={options} valueRadio={valueRadio} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx index b79c4e41f..560ca88bb 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx @@ -19,7 +19,7 @@ import Typography from '@mui/material/Typography'; import type { QuestionnaireItemAnswerOption } from 'fhir/r4'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import { ChoiceItemControl } from '../../../interfaces/choice.enum'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -51,7 +51,7 @@ function ChoiceRadioAnswerOptionItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = renderingExtensions; @@ -108,6 +108,7 @@ function ChoiceRadioAnswerOptionItem(props: BaseItemProps) { options={options} valueChoice={valueChoice} feedback={feedback} + feedbackSeverity={feedbackSeverity} isRepeated={isRepeated} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} @@ -128,6 +129,7 @@ function ChoiceRadioAnswerOptionItem(props: BaseItemProps) { options={options} valueChoice={valueChoice} feedback={feedback} + feedbackSeverity={feedbackSeverity} isRepeated={isRepeated} isTabled={isTabled} readOnly={readOnly} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionView.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionView.tsx index 90d1b8669..6df1d2935 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionView.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionView.tsx @@ -27,6 +27,7 @@ interface ChoiceRadioAnswerOptionViewProps extends PropsWithIsRepeatedAttribute options: QuestionnaireItemAnswerOption[]; valueChoice: string | null; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -43,6 +44,7 @@ function ChoiceRadioAnswerOptionView(props: ChoiceRadioAnswerOptionViewProps) { options, valueChoice, feedback, + feedbackSeverity, isRepeated, readOnly, expressionUpdated, @@ -61,6 +63,7 @@ function ChoiceRadioAnswerOptionView(props: ChoiceRadioAnswerOptionViewProps) { options={options} valueRadio={valueChoice} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} @@ -88,6 +91,7 @@ function ChoiceRadioAnswerOptionView(props: ChoiceRadioAnswerOptionViewProps) { options={options} valueRadio={valueChoice} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx index 268cb2f64..dca379ff7 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx @@ -27,6 +27,7 @@ interface ChoiceRadioAnswerValueSetFieldsProps { options: QuestionnaireItemAnswerOption[]; valueRadio: string | null; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -43,6 +44,7 @@ function ChoiceRadioAnswerValueSetFields(props: ChoiceRadioAnswerValueSetFieldsP options, valueRadio, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -60,6 +62,7 @@ function ChoiceRadioAnswerValueSetFields(props: ChoiceRadioAnswerValueSetFieldsP options={options} valueRadio={valueRadio} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx index 566809021..2e8c1e0cd 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx @@ -18,7 +18,7 @@ import { useMemo } from 'react'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -62,7 +62,7 @@ function ChoiceRadioAnswerValueSetItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = renderingExtensions; const instructionsId = displayInstructions && !feedback ? `instructions-${qItem.linkId}` : undefined; @@ -94,6 +94,7 @@ function ChoiceRadioAnswerValueSetItem(props: BaseItemProps) { options={options} valueRadio={valueRadio} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || dynamicCodingsUpdated || answerOptionsToggleExpUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} @@ -122,6 +123,7 @@ function ChoiceRadioAnswerValueSetItem(props: BaseItemProps) { options={options} valueRadio={valueRadio} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={ calcExpUpdated || dynamicCodingsUpdated || answerOptionsToggleExpUpdated diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx index a35b01df8..bd0a972b8 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx @@ -40,6 +40,7 @@ interface ChoiceSelectAnswerOptionFieldsProps options: QuestionnaireItemAnswerOption[]; valueSelect: QuestionnaireItemAnswerOption | null; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -53,6 +54,7 @@ function ChoiceSelectAnswerOptionFields(props: ChoiceSelectAnswerOptionFieldsPro options, valueSelect, feedback, + feedbackSeverity, readOnly, expressionUpdated, isTabled, @@ -75,7 +77,7 @@ function ChoiceSelectAnswerOptionFields(props: ChoiceSelectAnswerOptionFieldsPro return ( {feedback ? ( - + {feedback} ) : null} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx index 06971e333..7a8718d54 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx @@ -19,7 +19,7 @@ import type { QuestionnaireItemAnswerOption } from 'fhir/r4'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useReadOnly from '../../../hooks/useReadOnly'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; import { findInAnswerOptions, getQrChoiceValue } from '../../../utils/choice'; @@ -45,7 +45,7 @@ function ChoiceSelectAnswerOptionItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); // Get instructions ID for aria-describedby const { displayInstructions } = useRenderingExtensions(qItem); @@ -94,6 +94,7 @@ function ChoiceSelectAnswerOptionItem(props: BaseItemProps) { options={options} valueChoice={valueChoice} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} isRepeated={isRepeated} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionView.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionView.tsx index b94de8065..facb05e34 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionView.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionView.tsx @@ -36,6 +36,7 @@ interface ChoiceSelectAnswerOptionViewProps options: QuestionnaireItemAnswerOption[]; valueChoice: string | null; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -50,6 +51,7 @@ function ChoiceSelectAnswerOptionView(props: ChoiceSelectAnswerOptionViewProps) options, valueChoice, feedback, + feedbackSeverity, isRepeated, isTabled, renderingExtensions, @@ -73,6 +75,7 @@ function ChoiceSelectAnswerOptionView(props: ChoiceSelectAnswerOptionViewProps) options={options} valueSelect={valueSelect} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} isTabled={isTabled} @@ -100,6 +103,7 @@ function ChoiceSelectAnswerOptionView(props: ChoiceSelectAnswerOptionViewProps) options={options} valueSelect={valueSelect} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} isTabled={isTabled} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx index 50b08cf2b..774addd26 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx @@ -43,6 +43,7 @@ interface ChoiceSelectAnswerValueSetFieldsProps valueCoding: Coding | null; terminologyError: TerminologyError; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -57,6 +58,7 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField valueCoding, terminologyError, feedback, + feedbackSeverity, readOnly, expressionUpdated, isTabled, @@ -84,7 +86,7 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField if (codings.length > 0) { return ( {feedback ? ( - + {feedback} ) : null} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx index 077c8be39..31474a5f3 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx @@ -20,7 +20,7 @@ import { useEffect, useMemo } from 'react'; import type { Coding } from 'fhir/r4'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -57,7 +57,7 @@ function ChoiceSelectAnswerValueSetItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = renderingExtensions; const instructionsId = displayInstructions && !feedback ? `instructions-${qItem.linkId}` : undefined; @@ -110,6 +110,7 @@ function ChoiceSelectAnswerValueSetItem(props: BaseItemProps) { valueCoding={valueCoding} terminologyError={terminologyError} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || dynamicCodingsUpdated || answerOptionsToggleExpUpdated} isTabled={isTabled} @@ -136,6 +137,7 @@ function ChoiceSelectAnswerValueSetItem(props: BaseItemProps) { valueCoding={valueCoding} terminologyError={terminologyError} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || dynamicCodingsUpdated} isTabled={isTabled} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateField.tsx index 3cb87ff73..9bd5fcbd3 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateField.tsx @@ -32,6 +32,7 @@ interface CustomDateFieldProps extends PropsWithIsTabledAttribute { valueDate: string; input: string; feedback: string; + feedbackSeverity?: 'error' | 'warning'; isFocused: boolean; displayPrompt: string; entryFormat: string; @@ -52,6 +53,7 @@ function CustomDateField(props: CustomDateFieldProps) { valueDate, input, feedback, + feedbackSeverity, isFocused, displayPrompt, entryFormat, @@ -93,7 +95,7 @@ function CustomDateField(props: CustomDateFieldProps) { textFieldWidth={textFieldWidth} isTabled={isTabled} value={input} - error={!!feedback} + error={!!feedback && feedbackSeverity !== 'warning'} onChange={(e: ChangeEvent) => onInputChange(e.target.value)} placeholder={placeholderText} disabled={readOnly && readOnlyVisualStyle === 'disabled'} @@ -122,6 +124,9 @@ function CustomDateField(props: CustomDateFieldProps) { htmlInput: { ...(isTabled ? {} : { 'aria-label': itemText ?? `Unnamed ${itemType} item` }), ...(instructionsId && { 'aria-describedby': instructionsId }) + }, + formHelperText: { + sx: feedbackSeverity === 'warning' && !!feedback ? { color: 'warning.main' } : undefined } }} helperText={feedback} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateItem.tsx index 4cba130a6..b06ab968c 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateItem.tsx @@ -18,7 +18,7 @@ import { useState } from 'react'; import useDateValidation from '../../../../hooks/useDateValidation'; import useReadOnly from '../../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../../stores'; import { createEmptyQrItem, getQRItemId } from '../../../../utils/qrItem'; @@ -68,7 +68,10 @@ function CustomDateItem(props: BaseItemProps) { // Perform validation checks // Constraint and required feedback takes priority; fall back to date format feedback. - const validationFeedback = useValidationFeedback(qItem, undefined); + const { feedback: validationFeedback, feedbackSeverity } = useValidationFeedbackSeverity( + qItem, + undefined + ); const dateValidationFeedback = useDateValidation(input, dateParseFail); const feedback = validationFeedback || dateValidationFeedback; @@ -109,6 +112,7 @@ function CustomDateItem(props: BaseItemProps) { valueDate={displayDate} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} isFocused={focused} displayPrompt={displayPrompt} entryFormat={entryFormat} @@ -142,6 +146,7 @@ function CustomDateItem(props: BaseItemProps) { valueDate={displayDate} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} isFocused={focused} displayPrompt={displayPrompt} entryFormat={entryFormat} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx index e61207680..2b6892c2f 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomDateTimeItem.tsx @@ -23,7 +23,7 @@ import useDateNonEmptyValidation from '../../../../hooks/useDateTimeNonEmpty'; import useDateValidation from '../../../../hooks/useDateValidation'; import useReadOnly from '../../../../hooks/useReadOnly'; import useTimeValidation from '../../../../hooks/useTimeValidation'; -import useValidationFeedback from '../../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../../stores'; import { createEmptyQrItem, getQRItemId } from '../../../../utils/qrItem'; @@ -97,7 +97,10 @@ function CustomDateTimeItem(props: BaseItemProps) { // Perform validation checks // Constraint and required feedback takes priority; fall back to date/time format feedback. - const validationFeedback = useValidationFeedback(qItem, undefined); + const { feedback: validationFeedback, feedbackSeverity } = useValidationFeedbackSeverity( + qItem, + undefined + ); let dateFeedback = useDateValidation(dateInput, dateParseFail); const { timeFeedback, is24HourNotation } = useTimeValidation( timeInput, @@ -192,6 +195,7 @@ function CustomDateTimeItem(props: BaseItemProps) { periodInput={periodInput} is24HourNotation={is24HourNotation} dateFeedback={dateFeedback ?? ''} + dateFeedbackSeverity={feedbackSeverity} timeFeedback={timeFeedback ?? ''} dateFocused={dateFocused} displayPrompt={displayPrompt} @@ -230,6 +234,7 @@ function CustomDateTimeItem(props: BaseItemProps) { periodInput={periodInput} is24HourNotation={is24HourNotation} dateFeedback={dateFeedback ?? ''} + dateFeedbackSeverity={feedbackSeverity} timeFeedback={timeFeedback ?? ''} dateFocused={dateFocused} displayPrompt={displayPrompt} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/DateTimeField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/DateTimeField.tsx index 5df244658..fbd380271 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/DateTimeField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/DateTimeField.tsx @@ -31,6 +31,7 @@ interface DateTimeFieldProps extends PropsWithIsTabledAttribute { periodInput: string; is24HourNotation: boolean; dateFeedback: string; + dateFeedbackSeverity?: 'error' | 'warning'; timeFeedback: string; dateFocused: boolean; displayPrompt: string; @@ -55,6 +56,7 @@ function DateTimeField(props: DateTimeFieldProps) { periodInput, is24HourNotation, dateFeedback, + dateFeedbackSeverity, timeFeedback, dateFocused, displayPrompt, @@ -78,6 +80,7 @@ function DateTimeField(props: DateTimeFieldProps) { valueDate={displayDate} input={dateInput} feedback={dateFeedback ?? ''} + feedbackSeverity={dateFeedbackSeverity} isFocused={dateFocused} displayPrompt={displayPrompt} entryFormat={entryFormat} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalField.tsx index f5dc46a6f..f8af464f0 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalField.tsx @@ -30,6 +30,7 @@ interface DecimalFieldProps extends PropsWithIsTabledAttribute { qItem: QuestionnaireItem; input: string; feedback: string; + feedbackSeverity?: 'error' | 'warning'; renderingExtensions: RenderingExtensions; readOnly: boolean; calcExpUpdated: boolean; @@ -43,6 +44,7 @@ function DecimalField(props: DecimalFieldProps) { qItem, input, feedback, + feedbackSeverity, renderingExtensions, readOnly, calcExpUpdated, @@ -75,7 +77,7 @@ function DecimalField(props: DecimalFieldProps) { onInputChange(event.target.value)} disabled={readOnly && readOnlyVisualStyle === 'disabled'} @@ -91,6 +93,9 @@ function DecimalField(props: DecimalFieldProps) { ...(ariaLabel && { 'aria-label': ariaLabel }), ...(instructionsId && { 'aria-describedby': instructionsId }) }, + formHelperText: { + sx: feedbackSeverity === 'warning' && !!feedback ? { color: 'warning.main' } : undefined + }, input: { readOnly: readOnly && readOnlyVisualStyle === 'readonly', endAdornment: ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx index 3bf939ca3..3c9f6ebd8 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/DecimalItem/DecimalItem.tsx @@ -18,7 +18,7 @@ import debounce from 'lodash.debounce'; import { useCallback, useState } from 'react'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -62,7 +62,7 @@ function DecimalItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - there's no string-based input here - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); @@ -127,6 +127,7 @@ function DecimalItem(props: BaseItemProps) { qItem={qItem} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} renderingExtensions={renderingExtensions} readOnly={readOnly} calcExpUpdated={calcExpUpdated} @@ -153,6 +154,7 @@ function DecimalItem(props: BaseItemProps) { qItem={qItem} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} renderingExtensions={renderingExtensions} readOnly={readOnly} calcExpUpdated={calcExpUpdated} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerField.tsx index c87b083e9..7afc075fd 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerField.tsx @@ -31,6 +31,7 @@ interface IntegerFieldProps extends PropsWithIsTabledAttribute { qItem: QuestionnaireItem; input: string; feedback: string; + feedbackSeverity?: 'error' | 'warning'; renderingExtensions: RenderingExtensions; readOnly: boolean; calcExpUpdated: boolean; @@ -44,6 +45,7 @@ function IntegerField(props: IntegerFieldProps) { qItem, input, feedback, + feedbackSeverity, renderingExtensions, readOnly, calcExpUpdated, @@ -76,7 +78,7 @@ function IntegerField(props: IntegerFieldProps) { {feedback}} onChange={(event) => onInputChange(event.target.value)} disabled={readOnly && readOnlyVisualStyle === 'disabled'} @@ -93,6 +95,9 @@ function IntegerField(props: IntegerFieldProps) { ...(ariaLabel && { 'aria-label': ariaLabel }), ...(instructionsId && { 'aria-describedby': instructionsId }) }, + formHelperText: { + sx: feedbackSeverity === 'warning' && !!feedback ? { color: 'warning.main' } : undefined + }, input: { readOnly: readOnly && readOnlyVisualStyle === 'readonly', endAdornment: ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx index 92a07e265..209287f23 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/IntegerItem/IntegerItem.tsx @@ -18,7 +18,7 @@ import debounce from 'lodash.debounce'; import { useCallback, useState } from 'react'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -56,7 +56,7 @@ function IntegerItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); @@ -111,6 +111,7 @@ function IntegerItem(props: BaseItemProps) { qItem={qItem} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} renderingExtensions={renderingExtensions} readOnly={readOnly} calcExpUpdated={calcExpUpdated} @@ -137,6 +138,7 @@ function IntegerItem(props: BaseItemProps) { qItem={qItem} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} renderingExtensions={renderingExtensions} readOnly={readOnly} calcExpUpdated={calcExpUpdated} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/CheckboxFormGroup.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/CheckboxFormGroup.tsx index cb11162d9..27ab81b6e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/CheckboxFormGroup.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/CheckboxFormGroup.tsx @@ -19,6 +19,7 @@ interface ChoiceCheckboxFormGroupProps { options: QuestionnaireItemAnswerOption[]; answers: QuestionnaireResponseItemAnswer[]; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -35,6 +36,7 @@ function CheckboxFormGroup(props: ChoiceCheckboxFormGroupProps) { options, answers, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -94,7 +96,12 @@ function CheckboxFormGroup(props: ChoiceCheckboxFormGroupProps) { - {feedback ? {feedback} : null} + {feedback ? ( + + {feedback} + + ) : null} ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/RadioFormGroup.tsx b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/RadioFormGroup.tsx index 5b0d703a5..3b7ec1e85 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/RadioFormGroup.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/ItemParts/RadioFormGroup.tsx @@ -15,6 +15,7 @@ interface ChoiceRadioGroupProps { options: QuestionnaireItemAnswerOption[]; valueRadio: string | null; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -31,6 +32,7 @@ function RadioFormGroup(props: ChoiceRadioGroupProps) { options, valueRadio, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -99,7 +101,12 @@ function RadioFormGroup(props: ChoiceRadioGroupProps) { - {feedback ? {feedback} : null} + {feedback ? ( + + {feedback} + + ) : null} ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx index 319341c10..d369a05c6 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceAutocompleteItem.tsx @@ -31,7 +31,7 @@ import OpenChoiceAutocompleteField from './OpenChoiceAutocompleteField'; import ItemFieldGrid, { getInstructionsId } from '../ItemParts/ItemFieldGrid'; import ItemLabel from '../ItemParts/ItemLabel'; import { sanitizeInput } from '../../../utils/inputSanitization'; -import { useValidationFeedback } from '../../../hooks'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import { createEmptyQrItem } from '../../../utils'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -75,7 +75,8 @@ function OpenChoiceAutocompleteItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const validationFeedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback: validationFeedback, feedbackSeverity: validationFeedbackSeverity } = + useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); const instructionsId = getInstructionsId(qItem, displayInstructions, !!validationFeedback); @@ -89,7 +90,7 @@ function OpenChoiceAutocompleteItem(props: BaseItemProps) { if (terminologyFeedback) { feedback = terminologyFeedback; } else if (validationFeedback !== '') { - feedback = { message: validationFeedback, color: 'error' }; + feedback = { message: validationFeedback, color: validationFeedbackSeverity }; } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx index aeb04715c..100441bea 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx @@ -31,6 +31,7 @@ interface OpenChoiceCheckboxAnswerOptionFieldsProps { openLabelValue: string; openLabelChecked: boolean; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -51,6 +52,7 @@ function OpenChoiceCheckboxAnswerOptionFields(props: OpenChoiceCheckboxAnswerOpt openLabelValue, openLabelChecked, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -68,6 +70,7 @@ function OpenChoiceCheckboxAnswerOptionFields(props: OpenChoiceCheckboxAnswerOpt options={options} answers={answers} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx index 050d56497..a75441260 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx @@ -20,7 +20,7 @@ import { useCallback } from 'react'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useOpenLabel from '../../../hooks/useOpenLabel'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; import { updateChoiceCheckboxAnswers } from '../../../utils/choice'; @@ -57,7 +57,7 @@ function OpenChoiceCheckboxAnswerOptionItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = renderingExtensions; const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback); const openLabelText = getOpenLabelText(qItem); @@ -160,6 +160,7 @@ function OpenChoiceCheckboxAnswerOptionItem(props: BaseItemProps) { openLabelValue={openLabelValue} openLabelChecked={openLabelChecked} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} @@ -194,6 +195,7 @@ function OpenChoiceCheckboxAnswerOptionItem(props: BaseItemProps) { openLabelValue={openLabelValue} openLabelChecked={openLabelChecked} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetFields.tsx index 4f3c62d60..45fdaebab 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetFields.tsx @@ -35,6 +35,7 @@ interface OpenChoiceCheckboxFieldsProps { openLabelValue: string; openLabelChecked: boolean; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -56,6 +57,7 @@ function OpenChoiceCheckboxAnswerValueSetFields(props: OpenChoiceCheckboxFieldsP openLabelValue, openLabelChecked, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -75,6 +77,7 @@ function OpenChoiceCheckboxAnswerValueSetFields(props: OpenChoiceCheckboxFieldsP options={options} answers={answers} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetItem.tsx index 9e3281f75..4241a75ad 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerValueSetItem.tsx @@ -20,7 +20,7 @@ import { useCallback, useMemo } from 'react'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useOpenLabel from '../../../hooks/useOpenLabel'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -58,7 +58,7 @@ function OpenChoiceCheckboxAnswerValueSetItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = renderingExtensions; const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback); const openLabelText = getOpenLabelText(qItem); @@ -164,6 +164,7 @@ function OpenChoiceCheckboxAnswerValueSetItem(props: BaseItemProps) { openLabelValue={openLabelValue} openLabelChecked={openLabelChecked} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={ calcExpUpdated || dynamicCodingsUpdated || answerOptionsToggleExpUpdated @@ -201,6 +202,7 @@ function OpenChoiceCheckboxAnswerValueSetItem(props: BaseItemProps) { openLabelValue={openLabelValue} openLabelChecked={openLabelChecked} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={ calcExpUpdated || dynamicCodingsUpdated || answerOptionsToggleExpUpdated diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx index b6870eaf4..7560b01b9 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx @@ -27,6 +27,7 @@ interface OpenChoiceRadioAnswerOptionFieldsProps { openLabelValue: string | null; openLabelSelected: boolean; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -45,6 +46,7 @@ function OpenChoiceRadioAnswerOptionFields(props: OpenChoiceRadioAnswerOptionFie openLabelValue, openLabelSelected, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -61,6 +63,7 @@ function OpenChoiceRadioAnswerOptionFields(props: OpenChoiceRadioAnswerOptionFie options={options} valueRadio={valueRadio} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx index 4a65cc1b8..edd5992e1 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx @@ -19,7 +19,7 @@ import { useState } from 'react'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useReadOnly from '../../../hooks/useReadOnly'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; import { findInAnswerOptions, getQrChoiceValue } from '../../../utils/choice'; @@ -54,7 +54,7 @@ function OpenChoiceRadioAnswerOptionItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback); @@ -168,6 +168,7 @@ function OpenChoiceRadioAnswerOptionItem(props: BaseItemProps) { openLabelValue={openLabelValue} openLabelSelected={openLabelSelected} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={calcExpUpdated || answerOptionsToggleExpUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetFields.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetFields.tsx index 28347dd2d..03073de5e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetFields.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetFields.tsx @@ -31,6 +31,7 @@ interface OpenChoiceRadioAnswerValueSetFieldsProps { openLabelValue: string | null; openLabelSelected: boolean; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; expressionUpdated: boolean; answerOptionsToggleExpressionsMap: Map; @@ -50,6 +51,7 @@ function OpenChoiceRadioAnswerValueSetFields(props: OpenChoiceRadioAnswerValueSe openLabelValue, openLabelSelected, feedback, + feedbackSeverity, readOnly, expressionUpdated, answerOptionsToggleExpressionsMap, @@ -67,6 +69,7 @@ function OpenChoiceRadioAnswerValueSetFields(props: OpenChoiceRadioAnswerValueSe options={options} valueRadio={valueRadio} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={expressionUpdated} answerOptionsToggleExpressionsMap={answerOptionsToggleExpressionsMap} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetItem.tsx index 60a82c0bb..15f22fe6f 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerValueSetItem.tsx @@ -19,7 +19,7 @@ import { useMemo, useState } from 'react'; import useAnswerOptionsToggleExpressions from '../../../hooks/useAnswerOptionsToggleExpressions'; import useReadOnly from '../../../hooks/useReadOnly'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -59,7 +59,7 @@ function OpenChoiceRadioAnswerValueSetItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback); @@ -176,6 +176,7 @@ function OpenChoiceRadioAnswerValueSetItem(props: BaseItemProps) { openLabelValue={openLabelValue} openLabelSelected={openLabelSelected} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} expressionUpdated={ calcExpUpdated || dynamicCodingsUpdated || answerOptionsToggleExpUpdated diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx index 3c7c2ee3b..a235a2aec 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx @@ -42,6 +42,7 @@ interface OpenChoiceSelectAnswerOptionFieldProps options: QuestionnaireItemAnswerOption[]; valueSelect: QuestionnaireItemAnswerOption | null; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; calcExpUpdated: boolean; instructionsId?: string; @@ -57,6 +58,7 @@ function OpenChoiceSelectAnswerOptionField(props: OpenChoiceSelectAnswerOptionFi options, valueSelect, feedback, + feedbackSeverity, readOnly, calcExpUpdated, instructionsId, @@ -74,7 +76,7 @@ function OpenChoiceSelectAnswerOptionField(props: OpenChoiceSelectAnswerOptionFi return ( {feedback ? ( - + {feedback} ) : null} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx index 714e17709..ffdeba266 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionItem.tsx @@ -19,7 +19,7 @@ import type { AutocompleteChangeReason } from '@mui/material'; import type { QuestionnaireItemAnswerOption } from 'fhir/r4'; import useReadOnly from '../../../hooks/useReadOnly'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; import { createEmptyQrItem, getQRItemId } from '../../../utils/qrItem'; @@ -47,7 +47,7 @@ function OpenChoiceSelectAnswerOptionItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback); @@ -112,6 +112,7 @@ function OpenChoiceSelectAnswerOptionItem(props: BaseItemProps) { options={answerOptions} valueSelect={valueSelect} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} calcExpUpdated={calcExpUpdated} isTabled={isTabled} @@ -138,6 +139,7 @@ function OpenChoiceSelectAnswerOptionItem(props: BaseItemProps) { options={answerOptions} valueSelect={valueSelect} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} calcExpUpdated={calcExpUpdated} isTabled={isTabled} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx index 4c93f7a7e..68903b62c 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx @@ -43,6 +43,7 @@ interface OpenChoiceSelectAnswerValueSetFieldProps valueSelect: Coding | string | null; terminologyError: TerminologyError; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; calcExpUpdated: boolean; instructionsId?: string; @@ -59,6 +60,7 @@ function OpenChoiceSelectAnswerValueSetField(props: OpenChoiceSelectAnswerValueS valueSelect, terminologyError, feedback, + feedbackSeverity, readOnly, calcExpUpdated, instructionsId, @@ -74,7 +76,7 @@ function OpenChoiceSelectAnswerValueSetField(props: OpenChoiceSelectAnswerValueS return ( + {feedback} ) : null} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx index 453100672..c0cc85f25 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetItem.tsx @@ -19,7 +19,7 @@ import type { AutocompleteChangeReason } from '@mui/material'; import type { Coding } from 'fhir/r4'; import useReadOnly from '../../../hooks/useReadOnly'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useValueSetCodings from '../../../hooks/useValueSetCodings'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -48,7 +48,7 @@ function OpenChoiceSelectAnswerValueSetItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback); @@ -130,6 +130,7 @@ function OpenChoiceSelectAnswerValueSetItem(props: BaseItemProps) { valueSelect={valueSelect} terminologyError={terminologyError} feedback={feedback} + feedbackSeverity={feedbackSeverity} isTabled={isTabled} renderingExtensions={renderingExtensions} readOnly={readOnly} @@ -157,6 +158,7 @@ function OpenChoiceSelectAnswerValueSetItem(props: BaseItemProps) { valueSelect={valueSelect} terminologyError={terminologyError} feedback={feedback} + feedbackSeverity={feedbackSeverity} isTabled={isTabled} renderingExtensions={renderingExtensions} readOnly={readOnly} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityField.tsx index 929eee4d7..46de368b0 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityField.tsx @@ -29,6 +29,7 @@ interface QuantityFieldProps extends PropsWithIsTabledAttribute { itemText?: string; input: string; feedback: string; + feedbackSeverity?: 'error' | 'warning'; displayPrompt: string; displayUnit: string; entryFormat: string; @@ -45,6 +46,7 @@ function QuantityField(props: QuantityFieldProps) { itemText, input, feedback, + feedbackSeverity, displayPrompt, displayUnit, entryFormat, @@ -76,7 +78,7 @@ function QuantityField(props: QuantityFieldProps) { onInputChange(event.target.value)} disabled={readOnly && readOnlyVisualStyle === 'disabled'} placeholder={placeholderText} @@ -91,6 +93,9 @@ function QuantityField(props: QuantityFieldProps) { ...(ariaLabel && { 'aria-label': ariaLabel }), ...(instructionsId && { 'aria-describedby': instructionsId }) }, + formHelperText: { + sx: feedbackSeverity === 'warning' && !!feedback ? { color: 'warning.main' } : undefined + }, input: { readOnly: readOnly && readOnlyVisualStyle === 'readonly', endAdornment: ( diff --git a/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx index be303fb81..a6523068a 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/QuantityItem/QuantityItem.tsx @@ -19,7 +19,7 @@ import { useCallback, useMemo, useState } from 'react'; import type { Quantity, QuestionnaireItemAnswerOption } from 'fhir/r4'; import debounce from 'lodash.debounce'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; @@ -113,7 +113,7 @@ function QuantityItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); @@ -211,6 +211,7 @@ function QuantityItem(props: BaseItemProps) { itemText={qItem.text} input={valueInput} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} displayPrompt={displayPrompt} displayUnit={displayUnit} entryFormat={entryFormat} @@ -265,6 +266,7 @@ function QuantityItem(props: BaseItemProps) { itemText={qItem.text} input={valueInput} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} displayPrompt={displayPrompt} displayUnit={displayUnit} entryFormat={entryFormat} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx index 8c274afac..869e4a922 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderField.tsx @@ -37,6 +37,7 @@ interface SliderFieldProps extends PropsWithIsTabledAttribute { stepValue: number; isInteracted: boolean; feedback: string; + feedbackSeverity?: 'error' | 'warning'; readOnly: boolean; instructionsId?: string; onValueChange: (newValue: number) => void; @@ -55,6 +56,7 @@ function SliderField(props: SliderFieldProps) { maxLabel, isInteracted, feedback, + feedbackSeverity, readOnly, instructionsId, isTabled, @@ -109,7 +111,12 @@ function SliderField(props: SliderFieldProps) { /> - {feedback ? {feedback} : null} + {feedback ? ( + + {feedback} + + ) : null} ); } diff --git a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx index 75ef608cd..3fa964d51 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/SliderItem/SliderItem.tsx @@ -25,7 +25,7 @@ import SliderField from './SliderField'; import useSliderExtensions from '../../../hooks/useSliderExtensions'; import Box from '@mui/material/Box'; import { useQuestionnaireStore } from '../../../stores'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; import ItemLabel from '../ItemParts/ItemLabel'; @@ -60,7 +60,7 @@ function SliderItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); const instructionsId = displayInstructions && !feedback ? `instructions-${qItem.linkId}` : undefined; @@ -88,6 +88,7 @@ function SliderItem(props: BaseItemProps) { maxLabel={maxLabel} isInteracted={isInteracted} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} isTabled={isTabled} instructionsId={instructionsId} @@ -122,6 +123,7 @@ function SliderItem(props: BaseItemProps) { maxLabel={maxLabel} isInteracted={isInteracted} feedback={feedback} + feedbackSeverity={feedbackSeverity} readOnly={readOnly} isTabled={isTabled} instructionsId={instructionsId} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringField.tsx index 3fb3b1306..7638d9636 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringField.tsx @@ -31,6 +31,7 @@ interface StringFieldProps extends PropsWithIsTabledAttribute { qItem: QuestionnaireItem; input: string; feedback: string; + feedbackSeverity?: 'error' | 'warning'; renderingExtensions: RenderingExtensions; readOnly: boolean; calcExpUpdated: boolean; @@ -44,6 +45,7 @@ function StringField(props: StringFieldProps) { qItem, input, feedback, + feedbackSeverity, renderingExtensions, readOnly, isTabled, @@ -66,7 +68,7 @@ function StringField(props: StringFieldProps) { textFieldWidth={textFieldWidth} isTabled={isTabled} value={input} - error={!!feedback} + error={!!feedback && feedbackSeverity !== 'warning'} onChange={(event) => onInputChange(event.target.value)} placeholder={entryFormat || displayPrompt} disabled={readOnly && readOnlyVisualStyle === 'disabled'} @@ -94,6 +96,9 @@ function StringField(props: StringFieldProps) { }, htmlInput: { ...(instructionsId && { 'aria-describedby': instructionsId }) + }, + formHelperText: { + sx: feedbackSeverity === 'warning' && !!feedback ? { color: 'warning.main' } : undefined } }} helperText={{feedback}} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx index 1b62257aa..51e8fe829 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/StringItem/StringItem.tsx @@ -19,7 +19,7 @@ import type { QuestionnaireResponseItem } from 'fhir/r4'; import debounce from 'lodash.debounce'; import { useCallback, useState } from 'react'; import useReadOnly from '../../../hooks/useReadOnly'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; import { useQuestionnaireStore } from '../../../stores'; import { DEBOUNCE_DURATION } from '../../../utils/debounce'; @@ -57,7 +57,7 @@ function StringItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); // Get instructions ID for aria-describedby const { displayInstructions } = useRenderingExtensions(qItem); @@ -109,6 +109,7 @@ function StringItem(props: BaseItemProps) { qItem={qItem} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} renderingExtensions={renderingExtensions} readOnly={readOnly} calcExpUpdated={calcExpUpdated} @@ -134,6 +135,7 @@ function StringItem(props: BaseItemProps) { qItem={qItem} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} renderingExtensions={renderingExtensions} readOnly={readOnly} calcExpUpdated={calcExpUpdated} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextField.tsx index 0d83f63ac..d19510635 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextField.tsx @@ -30,6 +30,7 @@ interface TextFieldProps { qItem: QuestionnaireItem; input: string; feedback: string; + feedbackSeverity?: 'error' | 'warning'; renderingExtensions: RenderingExtensions; readOnly: boolean; calcExpUpdated: boolean; @@ -43,6 +44,7 @@ function TextField(props: TextFieldProps) { qItem, input, feedback, + feedbackSeverity, renderingExtensions, readOnly, calcExpUpdated, @@ -60,7 +62,7 @@ function TextField(props: TextFieldProps) { onInputChange(event.target.value)} disabled={readOnly && readOnlyVisualStyle === 'disabled'} placeholder={entryFormat || displayPrompt} @@ -85,6 +87,9 @@ function TextField(props: TextFieldProps) { }, htmlInput: { ...(instructionsId && { 'aria-describedby': instructionsId }) + }, + formHelperText: { + sx: feedbackSeverity === 'warning' && !!feedback ? { color: 'warning.main' } : undefined } }} helperText={{feedback}} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx index e2b73c26e..867113077 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/TextItem/TextItem.tsx @@ -17,7 +17,7 @@ import { useCallback, useState } from 'react'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import debounce from 'lodash.debounce'; import { createEmptyQrItem, getQRItemId } from '../../../utils/qrItem'; import { DEBOUNCE_DURATION } from '../../../utils/debounce'; @@ -55,7 +55,7 @@ function TextItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); // Get instructions ID for aria-describedby const { displayInstructions } = useRenderingExtensions(qItem); @@ -107,6 +107,7 @@ function TextItem(props: BaseItemProps) { qItem={qItem} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} renderingExtensions={renderingExtensions} readOnly={readOnly} calcExpUpdated={calcExpUpdated} @@ -132,6 +133,7 @@ function TextItem(props: BaseItemProps) { qItem={qItem} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} renderingExtensions={renderingExtensions} readOnly={readOnly} calcExpUpdated={calcExpUpdated} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlField.tsx b/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlField.tsx index 857983e6e..ed5106b6c 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlField.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlField.tsx @@ -28,6 +28,7 @@ interface UrlFieldProps extends PropsWithIsTabledAttribute { itemType: string; input: string; feedback: string; + feedbackSeverity?: 'error' | 'warning'; displayPrompt: string; displayUnit: string; entryFormat: string; @@ -42,6 +43,7 @@ function UrlField(props: UrlFieldProps) { itemType, input, feedback, + feedbackSeverity, displayPrompt, displayUnit, entryFormat, @@ -62,7 +64,7 @@ function UrlField(props: UrlFieldProps) { textFieldWidth={textFieldWidth} isTabled={isTabled} value={input} - error={!!feedback} + error={!!feedback && feedbackSeverity !== 'warning'} onChange={(event) => onInputChange(event.target.value)} placeholder={entryFormat || displayPrompt} disabled={readOnly && readOnlyVisualStyle === 'disabled'} @@ -84,6 +86,9 @@ function UrlField(props: UrlFieldProps) { }, htmlInput: { ...(instructionsId && { 'aria-describedby': instructionsId }) + }, + formHelperText: { + sx: feedbackSeverity === 'warning' && !!feedback ? { color: 'warning.main' } : undefined } }} helperText={feedback} diff --git a/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx b/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx index 21a842e94..46ef3011e 100644 --- a/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx +++ b/packages/smart-forms-renderer/src/components/FormComponents/UrlItem/UrlItem.tsx @@ -17,7 +17,7 @@ import React, { useCallback, useState } from 'react'; import type { BaseItemProps } from '../../../interfaces/renderProps.interface'; -import useValidationFeedback from '../../../hooks/useValidationFeedback'; +import useValidationFeedbackSeverity from '../../../hooks/useValidationFeedbackSeverity'; import useRenderingExtensions from '../../../hooks/useRenderingExtensions'; import debounce from 'lodash.debounce'; import { createEmptyQrItem, getQRItemId } from '../../../utils/qrItem'; @@ -58,7 +58,7 @@ function UrlItem(props: BaseItemProps) { const readOnly = useReadOnly(qItem, parentIsReadOnly); // Perform validation checks - const feedback = useValidationFeedback(qItem, feedbackFromParent); + const { feedback, feedbackSeverity } = useValidationFeedbackSeverity(qItem, feedbackFromParent); const { displayInstructions } = useRenderingExtensions(qItem); @@ -95,6 +95,7 @@ function UrlItem(props: BaseItemProps) { itemType={qItem.type} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} displayPrompt={displayPrompt} displayUnit={displayUnit} entryFormat={entryFormat} @@ -121,6 +122,7 @@ function UrlItem(props: BaseItemProps) { itemType={qItem.type} input={input} feedback={feedback ?? ''} + feedbackSeverity={feedbackSeverity} displayPrompt={displayPrompt} displayUnit={displayUnit} entryFormat={entryFormat} diff --git a/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts index 205970bc4..380654954 100644 --- a/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts +++ b/packages/smart-forms-renderer/src/hooks/useValidationFeedback.ts @@ -16,187 +16,19 @@ */ import type { QuestionnaireItem } from 'fhir/r4'; -import { - getMaxQuantityValue, - getMaxQuantityValueFeedback, - getMaxValue, - getMaxValueFeedback, - getMinQuantityValue, - getMinQuantityValueFeedback, - getMinValue, - getMinValueFeedback, - getRegexValidation, - getRequiredFeedback -} from '../utils/extensions'; -import { useQuestionnaireResponseStore, useQuestionnaireStore } from '../stores'; -import { structuredDataCapture } from 'fhir-sdc-helpers'; +import useValidationFeedbackSeverity from './useValidationFeedbackSeverity'; +/** + * @deprecated Use {@link useValidationFeedbackSeverity} instead. + * This wrapper exists for backward compatibility and will be removed in the next major release. + * Switching to the new hook lets field components distinguish warning-level feedback + * (advisory, amber) from error-level feedback (blocking, red). + */ function useValidationFeedback( qItem: QuestionnaireItem, feedbackFromParent: string | undefined ): string { - const invalidItems = useQuestionnaireResponseStore.use.invalidItems(); - const requiredItemsIsHighlighted = useQuestionnaireResponseStore.use.requiredItemsIsHighlighted(); - - // Target constraint-based validation - const targetConstraints = useQuestionnaireStore.use.targetConstraints(); - const targetConstraintLinkIds = useQuestionnaireStore.use.targetConstraintLinkIds(); - const targetConstraintKeys = targetConstraintLinkIds[qItem.linkId]; - if (targetConstraintKeys && targetConstraintKeys.length > 0) { - for (const targetConstraintKey of targetConstraintKeys) { - const targetConstraint = targetConstraints[targetConstraintKey]; - if (targetConstraint) { - const { isInvalid, human } = targetConstraint; - if (isInvalid) { - return human; - } - } - } - } - - // Feedback from parent - if (feedbackFromParent) { - return feedbackFromParent; - } - - // Feedback from current item from QR invalidItems - const invalidOperationOutcome = invalidItems[qItem.linkId]; - - // Invalid items are not present, so no feedback returned - if (!invalidOperationOutcome) { - return ''; - } - - // There is an invalidOperationOutcome but no issues, something is wrong with the validation - should never happen - if (!invalidOperationOutcome.issue || invalidOperationOutcome.issue.length === 0) { - return 'Input is invalid but no specific issues are found. Please report this at https://github.com/aehrc/smart-forms/issues.'; - } - - // Required-based validation - // User needs to manually invoke required items to be highlighted - if (requiredItemsIsHighlighted) { - const requiredIssue = invalidOperationOutcome.issue.find((issue) => issue.code === 'required'); - if (requiredIssue) { - const requiredFeedback = getRequiredFeedback(qItem); - return requiredFeedback ?? 'This field is required.'; - } - } - - // Iterate through the issues but we only return the first validation feedback we find. - // Once that's resolved, the subsequent feedbacks will be returned. - for (const issue of invalidOperationOutcome.issue) { - const validationCode = issue?.details?.coding?.[0].code; - - // If no validation code is provided, something is wrong with the validation - should never happen - if (!validationCode) { - return 'Input is invalid but no specific issues are found. Please report this at https://github.com/aehrc/smart-forms/issues.'; - } - - // http://hl7.org/fhir/StructureDefinition/regex - if (validationCode === 'regex') { - const regexValidation = getRegexValidation(qItem); - if (regexValidation) { - return `Input should match the specified regex: ${regexValidation.expression}`; - } - - return 'Input should match the specified regex.'; - } - - // http://hl7.org/fhir/StructureDefinition/minLength - if (validationCode === 'minLength') { - const minLength = structuredDataCapture.getMinLength(qItem); - if (typeof minLength === 'number') { - return `Enter at least ${minLength} characters.`; - } - - return 'Input is below the minimum character limit.'; - } - - // Questionnaire.item.maxLength - if (validationCode === 'maxLength') { - const maxLength = qItem.maxLength; - if (typeof maxLength === 'number') { - return `Enter no more than ${maxLength} characters.`; - } - - return 'Input is above the maximum character limit.'; - } - - // http://hl7.org/fhir/StructureDefinition/maxDecimalPlaces - if (validationCode === 'maxDecimalPlaces') { - const maxDecimalPlaces = structuredDataCapture.getMaxDecimalPlaces(qItem); - if (typeof maxDecimalPlaces === 'number') { - return `Enter a number with no more than ${maxDecimalPlaces} decimal places.`; - } - - return 'Input has too many decimal places.'; - } - - // http://hl7.org/fhir/StructureDefinition/minValue - if (validationCode === 'minValue') { - const minValueFeedback = getMinValueFeedback(qItem); - if (minValueFeedback) { - return minValueFeedback; - } - - const minValue = getMinValue(qItem); - if (typeof minValue === 'string' || typeof minValue === 'number') { - return `Enter a value greater than or equal to ${minValue}.`; - } - - return 'Input is less than the minimum value allowed.'; - } - - // http://hl7.org/fhir/StructureDefinition/maxValue - if (validationCode === 'maxValue') { - const maxValueFeedback = getMaxValueFeedback(qItem); - if (maxValueFeedback) { - return maxValueFeedback; - } - - const maxValue = getMaxValue(qItem); - if (typeof maxValue === 'string' || typeof maxValue === 'number') { - return `Enter a value less than or equal to ${maxValue}.`; - } - - return 'Input exceeds the maximum value allowed.'; - } - - // http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-minQuantity - if (validationCode === 'minQuantityValue') { - const minQuantityFeedback = getMinQuantityValueFeedback(qItem); - if (minQuantityFeedback) { - return minQuantityFeedback; - } - - const minQuantityValue = getMinQuantityValue(qItem); - if (typeof minQuantityValue === 'number') { - return `Enter a quantity greater than or equal to ${minQuantityValue}.`; - } - - return 'Input is less than the minimum quantity allowed.'; - } - - // http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-maxQuantity - if (validationCode === 'maxQuantityValue') { - const maxQuantityFeedback = getMaxQuantityValueFeedback(qItem); - if (maxQuantityFeedback) { - return maxQuantityFeedback; - } - - const maxQuantityValue = getMaxQuantityValue(qItem); - if (typeof maxQuantityValue === 'number') { - return `Enter a quantity less than or equal to ${maxQuantityValue}.`; - } - - return 'Input exceeds the maximum quantity allowed.'; - } - - // No specific issue code, continue to the next issue - } - - // No specific issue code from all issues, fallback to empty string - return ''; + return useValidationFeedbackSeverity(qItem, feedbackFromParent).feedback; } export default useValidationFeedback; diff --git a/packages/smart-forms-renderer/src/hooks/useValidationFeedbackSeverity.ts b/packages/smart-forms-renderer/src/hooks/useValidationFeedbackSeverity.ts new file mode 100644 index 000000000..9ae6f38d2 --- /dev/null +++ b/packages/smart-forms-renderer/src/hooks/useValidationFeedbackSeverity.ts @@ -0,0 +1,247 @@ +/* + * Copyright 2025 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { QuestionnaireItem } from 'fhir/r4'; +import { + getMaxQuantityValue, + getMaxQuantityValueFeedback, + getMaxValue, + getMaxValueFeedback, + getMinQuantityValue, + getMinQuantityValueFeedback, + getMinValue, + getMinValueFeedback, + getRegexValidation, + getRequiredFeedback +} from '../utils/extensions'; +import { useQuestionnaireResponseStore, useQuestionnaireStore } from '../stores'; +import { structuredDataCapture } from 'fhir-sdc-helpers'; + +export type FeedbackSeverity = 'error' | 'warning'; + +export interface ValidationFeedbackWithSeverity { + feedback: string; + feedbackSeverity: FeedbackSeverity; +} + +/** + * Returns both the human-readable validation feedback message and its severity + * (`'error'` or `'warning'`), allowing field components to style advisory warnings + * differently from blocking errors. + * + * Use this hook in place of the deprecated {@link useValidationFeedback}. + */ +function useValidationFeedbackSeverity( + qItem: QuestionnaireItem, + feedbackFromParent: string | undefined +): ValidationFeedbackWithSeverity { + const invalidItems = useQuestionnaireResponseStore.use.invalidItems(); + const requiredItemsIsHighlighted = useQuestionnaireResponseStore.use.requiredItemsIsHighlighted(); + + // Target constraint-based validation — severity comes from the constraint definition + const targetConstraints = useQuestionnaireStore.use.targetConstraints(); + const targetConstraintLinkIds = useQuestionnaireStore.use.targetConstraintLinkIds(); + const targetConstraintKeys = targetConstraintLinkIds[qItem.linkId]; + if (targetConstraintKeys && targetConstraintKeys.length > 0) { + for (const targetConstraintKey of targetConstraintKeys) { + const targetConstraint = targetConstraints[targetConstraintKey]; + if (targetConstraint) { + const { isInvalid, human, severityCode } = targetConstraint; + if (isInvalid) { + return { feedback: human, feedbackSeverity: severityCode ?? 'error' }; + } + } + } + } + + // Feedback from parent — parent groups don't carry a severity, treat as error + if (feedbackFromParent) { + return { feedback: feedbackFromParent, feedbackSeverity: 'error' }; + } + + // Feedback from current item from QR invalidItems + const invalidOperationOutcome = invalidItems[qItem.linkId]; + + // No invalid items — no feedback + if (!invalidOperationOutcome) { + return { feedback: '', feedbackSeverity: 'error' }; + } + + // OperationOutcome present but no issues — internal error, should never happen + if (!invalidOperationOutcome.issue || invalidOperationOutcome.issue.length === 0) { + return { + feedback: + 'Input is invalid but no specific issues are found. Please report this at https://github.com/aehrc/smart-forms/issues.', + feedbackSeverity: 'error' + }; + } + + // Required-based validation — user must manually invoke required highlighting + if (requiredItemsIsHighlighted) { + const requiredIssue = invalidOperationOutcome.issue.find((issue) => issue.code === 'required'); + if (requiredIssue) { + const requiredFeedback = getRequiredFeedback(qItem); + const severity: FeedbackSeverity = requiredIssue.severity === 'warning' ? 'warning' : 'error'; + return { + feedback: requiredFeedback ?? 'This field is required.', + feedbackSeverity: severity + }; + } + } + + // Iterate through issues; return the first recognisable validation feedback together with + // the severity stored on that issue (set by createValidationOperationOutcomeIssue). + for (const issue of invalidOperationOutcome.issue) { + const validationCode = issue?.details?.coding?.[0].code; + + if (!validationCode) { + return { + feedback: + 'Input is invalid but no specific issues are found. Please report this at https://github.com/aehrc/smart-forms/issues.', + feedbackSeverity: 'error' + }; + } + + const severity: FeedbackSeverity = issue.severity === 'warning' ? 'warning' : 'error'; + + if (validationCode === 'regex') { + const regexValidation = getRegexValidation(qItem); + if (regexValidation) { + return { + feedback: `Input should match the specified regex: ${regexValidation.expression}`, + feedbackSeverity: severity + }; + } + return { feedback: 'Input should match the specified regex.', feedbackSeverity: severity }; + } + + if (validationCode === 'minLength') { + const minLength = structuredDataCapture.getMinLength(qItem); + if (typeof minLength === 'number') { + return { + feedback: `Enter at least ${minLength} characters.`, + feedbackSeverity: severity + }; + } + return { + feedback: 'Input is below the minimum character limit.', + feedbackSeverity: severity + }; + } + + if (validationCode === 'maxLength') { + const maxLength = qItem.maxLength; + if (typeof maxLength === 'number') { + return { + feedback: `Enter no more than ${maxLength} characters.`, + feedbackSeverity: severity + }; + } + return { + feedback: 'Input is above the maximum character limit.', + feedbackSeverity: severity + }; + } + + if (validationCode === 'maxDecimalPlaces') { + const maxDecimalPlaces = structuredDataCapture.getMaxDecimalPlaces(qItem); + if (typeof maxDecimalPlaces === 'number') { + return { + feedback: `Enter a number with no more than ${maxDecimalPlaces} decimal places.`, + feedbackSeverity: severity + }; + } + return { feedback: 'Input has too many decimal places.', feedbackSeverity: severity }; + } + + if (validationCode === 'minValue') { + const minValueFeedback = getMinValueFeedback(qItem); + if (minValueFeedback) { + return { feedback: minValueFeedback, feedbackSeverity: severity }; + } + const minValue = getMinValue(qItem); + if (typeof minValue === 'string' || typeof minValue === 'number') { + return { + feedback: `Enter a value greater than or equal to ${minValue}.`, + feedbackSeverity: severity + }; + } + return { + feedback: 'Input is less than the minimum value allowed.', + feedbackSeverity: severity + }; + } + + if (validationCode === 'maxValue') { + const maxValueFeedback = getMaxValueFeedback(qItem); + if (maxValueFeedback) { + return { feedback: maxValueFeedback, feedbackSeverity: severity }; + } + const maxValue = getMaxValue(qItem); + if (typeof maxValue === 'string' || typeof maxValue === 'number') { + return { + feedback: `Enter a value less than or equal to ${maxValue}.`, + feedbackSeverity: severity + }; + } + return { + feedback: 'Input exceeds the maximum value allowed.', + feedbackSeverity: severity + }; + } + + if (validationCode === 'minQuantityValue') { + const minQuantityFeedback = getMinQuantityValueFeedback(qItem); + if (minQuantityFeedback) { + return { feedback: minQuantityFeedback, feedbackSeverity: severity }; + } + const minQuantityValue = getMinQuantityValue(qItem); + if (typeof minQuantityValue === 'number') { + return { + feedback: `Enter a quantity greater than or equal to ${minQuantityValue}.`, + feedbackSeverity: severity + }; + } + return { + feedback: 'Input is less than the minimum quantity allowed.', + feedbackSeverity: severity + }; + } + + if (validationCode === 'maxQuantityValue') { + const maxQuantityFeedback = getMaxQuantityValueFeedback(qItem); + if (maxQuantityFeedback) { + return { feedback: maxQuantityFeedback, feedbackSeverity: severity }; + } + const maxQuantityValue = getMaxQuantityValue(qItem); + if (typeof maxQuantityValue === 'number') { + return { + feedback: `Enter a quantity less than or equal to ${maxQuantityValue}.`, + feedbackSeverity: severity + }; + } + return { + feedback: 'Input exceeds the maximum quantity allowed.', + feedbackSeverity: severity + }; + } + } + + return { feedback: '', feedbackSeverity: 'error' }; +} + +export default useValidationFeedbackSeverity; diff --git a/packages/smart-forms-renderer/src/test/useValidationFeedbackSeverity.test.ts b/packages/smart-forms-renderer/src/test/useValidationFeedbackSeverity.test.ts new file mode 100644 index 000000000..7ad4d58c8 --- /dev/null +++ b/packages/smart-forms-renderer/src/test/useValidationFeedbackSeverity.test.ts @@ -0,0 +1,268 @@ +/// +/// + +/* + * Copyright 2025 Commonwealth Scientific and Industrial Research + * Organisation (CSIRO) ABN 41 687 119 230. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderHook } from '@testing-library/react'; +import type { QuestionnaireItem, OperationOutcome } from 'fhir/r4'; +import useValidationFeedbackSeverity from '../hooks/useValidationFeedbackSeverity'; +import type { TargetConstraint } from '../interfaces/targetConstraint.interface'; + +// Mock dependencies +jest.mock('../utils/extensions', () => ({ + getMaxQuantityValue: jest.fn(), + getMaxQuantityValueFeedback: jest.fn(), + getMaxValue: jest.fn(), + getMaxValueFeedback: jest.fn(), + getMinQuantityValue: jest.fn(), + getMinQuantityValueFeedback: jest.fn(), + getMinValue: jest.fn(), + getMinValueFeedback: jest.fn(), + getRegexValidation: jest.fn(), + getRequiredFeedback: jest.fn() +})); + +jest.mock('fhir-sdc-helpers', () => ({ + structuredDataCapture: { + getMinLength: jest.fn(), + getMaxDecimalPlaces: jest.fn() + } +})); + +let mockInvalidItems: Record = {}; +let mockRequiredItemsIsHighlighted = false; +let mockTargetConstraints: Record = {}; +let mockTargetConstraintLinkIds: Record = {}; + +jest.mock('../stores', () => ({ + useQuestionnaireResponseStore: { + use: { + invalidItems: () => mockInvalidItems, + requiredItemsIsHighlighted: () => mockRequiredItemsIsHighlighted + } + }, + useQuestionnaireStore: { + use: { + targetConstraints: () => mockTargetConstraints, + targetConstraintLinkIds: () => mockTargetConstraintLinkIds + } + } +})); + +import { getRequiredFeedback, getRegexValidation } from '../utils/extensions'; + +const mockGetRequiredFeedback = getRequiredFeedback as jest.MockedFunction< + typeof getRequiredFeedback +>; +const mockGetRegexValidation = getRegexValidation as jest.MockedFunction; + +describe('useValidationFeedbackSeverity', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockInvalidItems = {}; + mockRequiredItemsIsHighlighted = false; + mockTargetConstraints = {}; + mockTargetConstraintLinkIds = {}; + mockGetRequiredFeedback.mockReturnValue(null); + mockGetRegexValidation.mockReturnValue(undefined); + }); + + describe('no validation issues', () => { + it('returns empty feedback and error severity when there are no issues', () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string' }; + + const { result } = renderHook(() => useValidationFeedbackSeverity(qItem, undefined)); + + expect(result.current.feedback).toBe(''); + expect(result.current.feedbackSeverity).toBe('error'); + }); + }); + + describe('target constraint severity', () => { + it('returns "error" severity when a target constraint with severityCode "error" is invalid', () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string' }; + + mockTargetConstraintLinkIds = { test: ['c1'] }; + mockTargetConstraints = { + c1: { + key: 'c1', + severityCode: 'error', + isInvalid: true, + human: 'Value must not be empty', + valueExpression: { language: 'text/fhirpath', expression: 'true' } + } + }; + + const { result } = renderHook(() => useValidationFeedbackSeverity(qItem, undefined)); + + expect(result.current.feedback).toBe('Value must not be empty'); + expect(result.current.feedbackSeverity).toBe('error'); + }); + + it('returns "warning" severity when a target constraint with severityCode "warning" is invalid', () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string' }; + + mockTargetConstraintLinkIds = { test: ['c1'] }; + mockTargetConstraints = { + c1: { + key: 'c1', + severityCode: 'warning', + isInvalid: true, + human: 'Consider adding a more specific value', + valueExpression: { language: 'text/fhirpath', expression: 'true' } + } + }; + + const { result } = renderHook(() => useValidationFeedbackSeverity(qItem, undefined)); + + expect(result.current.feedback).toBe('Consider adding a more specific value'); + expect(result.current.feedbackSeverity).toBe('warning'); + }); + + it('skips valid target constraints even if severity is warning', () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string' }; + + mockTargetConstraintLinkIds = { test: ['c1'] }; + mockTargetConstraints = { + c1: { + key: 'c1', + severityCode: 'warning', + isInvalid: false, + human: 'Should not appear', + valueExpression: { language: 'text/fhirpath', expression: 'true' } + } + }; + + const { result } = renderHook(() => useValidationFeedbackSeverity(qItem, undefined)); + + expect(result.current.feedback).toBe(''); + }); + }); + + describe('feedbackFromParent severity', () => { + it('always returns "error" severity for feedbackFromParent', () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string' }; + + const { result } = renderHook(() => + useValidationFeedbackSeverity(qItem, 'Parent group error') + ); + + expect(result.current.feedback).toBe('Parent group error'); + expect(result.current.feedbackSeverity).toBe('error'); + }); + }); + + describe('OperationOutcome issue severity', () => { + it('returns "error" severity when the matching issue has severity "error"', () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string' }; + + mockInvalidItems = { + test: { + resourceType: 'OperationOutcome', + issue: [ + { + severity: 'error', + code: 'invalid', + details: { coding: [{ code: 'regex' }] } + } + ] + } + }; + + mockGetRegexValidation.mockReturnValue(undefined); + + const { result } = renderHook(() => useValidationFeedbackSeverity(qItem, undefined)); + + expect(result.current.feedback).toBe('Input should match the specified regex.'); + expect(result.current.feedbackSeverity).toBe('error'); + }); + + it('returns "warning" severity when the matching issue has severity "warning"', () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string' }; + + mockInvalidItems = { + test: { + resourceType: 'OperationOutcome', + issue: [ + { + severity: 'warning', + code: 'invalid', + details: { coding: [{ code: 'regex' }] } + } + ] + } + }; + + mockGetRegexValidation.mockReturnValue(undefined); + + const { result } = renderHook(() => useValidationFeedbackSeverity(qItem, undefined)); + + expect(result.current.feedback).toBe('Input should match the specified regex.'); + expect(result.current.feedbackSeverity).toBe('warning'); + }); + + it('returns "warning" severity for a required field issue marked as warning', () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string' }; + + mockRequiredItemsIsHighlighted = true; + mockInvalidItems = { + test: { + resourceType: 'OperationOutcome', + issue: [ + { + severity: 'warning', + code: 'required', + details: { coding: [{ code: 'required' }] } + } + ] + } + }; + + mockGetRequiredFeedback.mockReturnValue('Advisory: this field is recommended'); + + const { result } = renderHook(() => useValidationFeedbackSeverity(qItem, undefined)); + + expect(result.current.feedback).toBe('Advisory: this field is recommended'); + expect(result.current.feedbackSeverity).toBe('warning'); + }); + }); + + describe('backward-compatible feedback text', () => { + it('returns identical feedback text to the deprecated useValidationFeedback hook', async () => { + const qItem: QuestionnaireItem = { linkId: 'test', type: 'string', maxLength: 5 }; + + mockInvalidItems = { + test: { + resourceType: 'OperationOutcome', + issue: [ + { + severity: 'error', + code: 'business-rule', + details: { coding: [{ code: 'maxLength' }] } + } + ] + } + }; + + const { result } = renderHook(() => useValidationFeedbackSeverity(qItem, undefined)); + + expect(result.current.feedback).toBe('Enter no more than 5 characters.'); + expect(result.current.feedbackSeverity).toBe('error'); + }); + }); +});