@aehrc/smart-forms-renderer 1.2.7 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/lib/components/FormComponents/AttachmentItem/AttachmentField.js +3 -2
  2. package/lib/components/FormComponents/AttachmentItem/AttachmentField.js.map +1 -1
  3. package/lib/components/FormComponents/BooleanItem/BooleanField.d.ts +2 -1
  4. package/lib/components/FormComponents/BooleanItem/BooleanField.js +6 -4
  5. package/lib/components/FormComponents/BooleanItem/BooleanField.js.map +1 -1
  6. package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.js +8 -8
  7. package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.js.map +1 -1
  8. package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.js +11 -11
  9. package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.js.map +1 -1
  10. package/lib/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateField.js +2 -1
  11. package/lib/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateField.js.map +1 -1
  12. package/lib/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomTimeField.js +2 -1
  13. package/lib/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomTimeField.js.map +1 -1
  14. package/lib/components/FormComponents/DecimalItem/DecimalField.js +2 -1
  15. package/lib/components/FormComponents/DecimalItem/DecimalField.js.map +1 -1
  16. package/lib/components/FormComponents/GroupItem/GroupHeading.js +1 -1
  17. package/lib/components/FormComponents/GroupItem/GroupHeading.js.map +1 -1
  18. package/lib/components/FormComponents/IntegerItem/IntegerField.js +2 -1
  19. package/lib/components/FormComponents/IntegerItem/IntegerField.js.map +1 -1
  20. package/lib/components/FormComponents/Item.styles.d.ts +1 -1
  21. package/lib/components/FormComponents/Item.styles.js +1 -1
  22. package/lib/components/FormComponents/ItemParts/AccessibleFeedback.d.ts +7 -0
  23. package/lib/components/FormComponents/ItemParts/AccessibleFeedback.js +7 -0
  24. package/lib/components/FormComponents/ItemParts/AccessibleFeedback.js.map +1 -0
  25. package/lib/components/FormComponents/ItemParts/CheckboxFormGroup.js +4 -2
  26. package/lib/components/FormComponents/ItemParts/CheckboxFormGroup.js.map +1 -1
  27. package/lib/components/FormComponents/ItemParts/ClearButtonAdornment.js +12 -12
  28. package/lib/components/FormComponents/ItemParts/ClearButtonAdornment.js.map +1 -1
  29. package/lib/components/FormComponents/ItemParts/RadioFormGroup.d.ts +1 -1
  30. package/lib/components/FormComponents/ItemParts/RadioFormGroup.js +5 -3
  31. package/lib/components/FormComponents/ItemParts/RadioFormGroup.js.map +1 -1
  32. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.js +8 -8
  33. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.js.map +1 -1
  34. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.js +3 -3
  35. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.js.map +1 -1
  36. package/lib/components/FormComponents/QuantityItem/QuantityField.js +2 -1
  37. package/lib/components/FormComponents/QuantityItem/QuantityField.js.map +1 -1
  38. package/lib/components/FormComponents/SliderItem/SliderField.js +4 -2
  39. package/lib/components/FormComponents/SliderItem/SliderField.js.map +1 -1
  40. package/lib/components/FormComponents/StringItem/StringField.js +2 -1
  41. package/lib/components/FormComponents/StringItem/StringField.js.map +1 -1
  42. package/lib/components/FormComponents/TextItem/TextField.js +2 -1
  43. package/lib/components/FormComponents/TextItem/TextField.js.map +1 -1
  44. package/lib/components/FormComponents/UrlItem/UrlField.js +2 -1
  45. package/lib/components/FormComponents/UrlItem/UrlField.js.map +1 -1
  46. package/lib/components/Tabs/FormBodySingleTab.js +6 -1
  47. package/lib/components/Tabs/FormBodySingleTab.js.map +1 -1
  48. package/package.json +4 -4
  49. package/src/components/FormComponents/AttachmentItem/AttachmentField.tsx +7 -2
  50. package/src/components/FormComponents/BooleanItem/BooleanField.tsx +12 -3
  51. package/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionFields.tsx +2 -3
  52. package/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetFields.tsx +3 -3
  53. package/src/components/FormComponents/DateTimeItems/CustomDateItem/CustomDateField.tsx +2 -1
  54. package/src/components/FormComponents/DateTimeItems/CustomDateTimeItem/CustomTimeField.tsx +6 -3
  55. package/src/components/FormComponents/DecimalItem/DecimalField.tsx +2 -1
  56. package/src/components/FormComponents/GroupItem/GroupHeading.tsx +8 -9
  57. package/src/components/FormComponents/IntegerItem/IntegerField.tsx +2 -1
  58. package/src/components/FormComponents/Item.styles.ts +1 -1
  59. package/src/components/FormComponents/ItemParts/AccessibleFeedback.tsx +35 -0
  60. package/src/components/FormComponents/ItemParts/CheckboxFormGroup.tsx +10 -2
  61. package/src/components/FormComponents/ItemParts/ClearButtonAdornment.tsx +22 -23
  62. package/src/components/FormComponents/ItemParts/RadioFormGroup.tsx +11 -3
  63. package/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerOptionField.tsx +2 -3
  64. package/src/components/FormComponents/OpenChoiceItems/OpenChoiceSelectAnswerValueSetField.tsx +2 -3
  65. package/src/components/FormComponents/QuantityItem/QuantityField.tsx +2 -1
  66. package/src/components/FormComponents/SliderItem/SliderField.tsx +10 -2
  67. package/src/components/FormComponents/StringItem/StringField.tsx +2 -1
  68. package/src/components/FormComponents/TextItem/TextField.tsx +2 -1
  69. package/src/components/FormComponents/UrlItem/UrlField.tsx +2 -1
  70. package/src/components/Tabs/FormBodySingleTab.tsx +28 -8
@@ -64,15 +64,6 @@ const GroupHeading = memo(function GroupHeading(props: GroupHeadingProps) {
64
64
  <>
65
65
  <Box display="flex" alignItems="center" width="100%">
66
66
  <Box position="relative" display="flex" flexGrow={1} alignItems="center">
67
- {/* Required asterisk position is in front of text */}
68
- {required && requiredIndicatorPosition === 'start' ? (
69
- <RequiredAsterisk
70
- sx={{ position: 'absolute', top: 0, left: -8 }} // Adjust top and left values as needed
71
- >
72
- *
73
- </RequiredAsterisk>
74
- ) : null}
75
-
76
67
  {/* Group Heading typography */}
77
68
  {/* flexGrow: 1 is important if xhtml and markdown rendering has width: 100% */}
78
69
  <Typography
@@ -82,6 +73,14 @@ const GroupHeading = memo(function GroupHeading(props: GroupHeadingProps) {
82
73
  display="flex"
83
74
  alignItems="center"
84
75
  sx={{ flexGrow: 1, ...(parentStyles || {}) }}>
76
+ {/* Required asterisk position is in front of text */}
77
+ {required && requiredIndicatorPosition === 'start' ? (
78
+ <RequiredAsterisk
79
+ sx={{ position: 'absolute', top: 0, left: -8 }} // Adjust top and left values as needed
80
+ >
81
+ *
82
+ </RequiredAsterisk>
83
+ ) : null}
85
84
  <ItemTextSwitcher qItem={qItem} />
86
85
 
87
86
  {/* Required asterisk position is behind text */}
@@ -25,6 +25,7 @@ import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
25
25
  import ItemRepopulateButton from '../ItemParts/ItemRepopulateButton';
26
26
  import type { RenderingExtensions } from '../../../hooks/useRenderingExtensions';
27
27
  import { StandardTextField } from '../Textfield.styles';
28
+ import AccessibleFeedback from '../ItemParts/AccessibleFeedback';
28
29
 
29
30
  interface IntegerFieldProps extends PropsWithIsTabledAttribute {
30
31
  qItem: QuestionnaireItem;
@@ -74,7 +75,7 @@ function IntegerField(props: IntegerFieldProps) {
74
75
  id={inputId}
75
76
  value={input}
76
77
  error={!!feedback}
77
- helperText={feedback}
78
+ helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
78
79
  onChange={(event) => onInputChange(event.target.value)}
79
80
  disabled={readOnly && readOnlyVisualStyle === 'disabled'}
80
81
  label={displayPrompt}
@@ -18,7 +18,7 @@
18
18
  import { styled } from '@mui/material/styles';
19
19
  import Typography from '@mui/material/Typography';
20
20
 
21
- export const StyledRequiredTypography = styled(Typography)(({ theme }) => ({
21
+ export const StyledFeedbackTypography = styled(Typography)(({ theme }) => ({
22
22
  color: theme.palette.error.main,
23
23
  fontSize: '0.75rem',
24
24
  marginTop: 4
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Copyright 2025 Commonwealth Scientific and Industrial Research
3
+ * Organisation (CSIRO) ABN 41 687 119 230.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import { type ReactNode } from 'react';
19
+
20
+ interface AccessibleFeedbackProps {
21
+ children: ReactNode;
22
+ id?: string;
23
+ }
24
+
25
+ function AccessibleFeedback(props: AccessibleFeedbackProps) {
26
+ const { children, id } = props;
27
+
28
+ return (
29
+ <span id={id} role="alert" aria-live="assertive">
30
+ {children}
31
+ </span>
32
+ );
33
+ }
34
+
35
+ export default AccessibleFeedback;
@@ -10,9 +10,10 @@ import { ChoiceItemOrientation } from '../../../interfaces/choice.enum';
10
10
  import { useRendererConfigStore } from '../../../stores';
11
11
  import { getChoiceOrientation } from '../../../utils/choice';
12
12
  import CheckboxOptionList from '../ChoiceItems/CheckboxOptionList';
13
- import { StyledRequiredTypography } from '../Item.styles';
13
+ import { StyledFeedbackTypography } from '../Item.styles';
14
14
  import ClearInputButton from './ClearInputButton';
15
15
  import ExpressionUpdateFadingIcon from './ExpressionUpdateFadingIcon';
16
+ import AccessibleFeedback from './AccessibleFeedback';
16
17
 
17
18
  interface ChoiceCheckboxFormGroupProps {
18
19
  qItem: QuestionnaireItem;
@@ -50,6 +51,8 @@ function CheckboxFormGroup(props: ChoiceCheckboxFormGroupProps) {
50
51
 
51
52
  const answersEmpty = answers.length === 0;
52
53
 
54
+ const feedbackId = `${qItem.type}-${qItem.linkId}-feedback`;
55
+
53
56
  return (
54
57
  <>
55
58
  <Box
@@ -68,6 +71,7 @@ function CheckboxFormGroup(props: ChoiceCheckboxFormGroupProps) {
68
71
  {...(!isTabled
69
72
  ? { 'aria-labelledby': 'label-' + qItem.linkId }
70
73
  : { 'aria-label': qItem.text ?? 'Unnamed checkbox list' })}
74
+ aria-describedby={feedback ? feedbackId : undefined}
71
75
  role="group"
72
76
  row={orientation === ChoiceItemOrientation.Horizontal}
73
77
  sx={inputsFlexGrow ? { width: '100%', flexWrap: 'nowrap' } : {}}>
@@ -91,7 +95,11 @@ function CheckboxFormGroup(props: ChoiceCheckboxFormGroupProps) {
91
95
  <ClearInputButton buttonShown={!answersEmpty} readOnly={readOnly} onClear={onClear} />
92
96
  </Box>
93
97
 
94
- {feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
98
+ {feedback ? (
99
+ <AccessibleFeedback id={feedbackId}>
100
+ <StyledFeedbackTypography>{feedback}</StyledFeedbackTypography>
101
+ </AccessibleFeedback>
102
+ ) : null}
95
103
  </>
96
104
  );
97
105
  }
@@ -14,29 +14,28 @@ export function ClearButtonAdornment(props: ClearButtonAdornmentProps) {
14
14
  }
15
15
 
16
16
  return (
17
- <span title="Clear">
18
- <IconButton
19
- aria-label="Clear"
20
- size="small"
21
- tabIndex={-1}
22
- onClick={(e) => {
23
- onClear();
17
+ <IconButton
18
+ aria-hidden="true"
19
+ tabIndex={-1}
20
+ size="small"
21
+ title="Clear"
22
+ onClick={(e) => {
23
+ onClear();
24
24
 
25
- // Manually re-focus the input field
26
- const input = e.currentTarget.closest('.MuiOutlinedInput-root')?.querySelector('input');
27
- if (input) {
28
- input.focus();
29
- }
30
- }}
31
- onMouseDown={(e) => {
32
- e.preventDefault(); // Prevent the button from stealing focus
33
- }}
34
- className="StandardTextField-clearIndicator"
35
- sx={{
36
- p: 0.5
37
- }}>
38
- <ClearIcon fontSize="small" />
39
- </IconButton>
40
- </span>
25
+ // Manually re-focus the input field
26
+ const input = e.currentTarget.closest('.MuiOutlinedInput-root')?.querySelector('input');
27
+ if (input) {
28
+ input.focus();
29
+ }
30
+ }}
31
+ onMouseDown={(e) => {
32
+ e.preventDefault(); // Prevent the button from stealing focus
33
+ }}
34
+ className="StandardTextField-clearIndicator"
35
+ sx={{
36
+ p: 0.5
37
+ }}>
38
+ <ClearIcon fontSize="small" />
39
+ </IconButton>
41
40
  );
42
41
  }
@@ -1,14 +1,15 @@
1
1
  import Box from '@mui/material/Box';
2
2
  import RadioGroup from '@mui/material/RadioGroup';
3
3
  import type { QuestionnaireItem, QuestionnaireItemAnswerOption } from 'fhir/r4';
4
- import type { ReactNode } from 'react';
4
+ import React, { ReactNode } from 'react';
5
5
  import { ChoiceItemOrientation } from '../../../interfaces/choice.enum';
6
6
  import { useRendererConfigStore } from '../../../stores';
7
7
  import { getChoiceOrientation } from '../../../utils/choice';
8
- import { StyledRequiredTypography } from '../Item.styles';
8
+ import { StyledFeedbackTypography } from '../Item.styles';
9
9
  import ClearInputButton from './ClearInputButton';
10
10
  import ExpressionUpdateFadingIcon from './ExpressionUpdateFadingIcon';
11
11
  import RadioOptionList from './RadioOptionList';
12
+ import AccessibleFeedback from './AccessibleFeedback';
12
13
 
13
14
  interface ChoiceRadioGroupProps {
14
15
  qItem: QuestionnaireItem;
@@ -44,6 +45,8 @@ function RadioFormGroup(props: ChoiceRadioGroupProps) {
44
45
 
45
46
  const orientation = getChoiceOrientation(qItem) ?? ChoiceItemOrientation.Vertical;
46
47
 
48
+ const feedbackId = qItem.type + '-' + qItem.linkId + '-feedback';
49
+
47
50
  return (
48
51
  <>
49
52
  <Box
@@ -64,6 +67,7 @@ function RadioFormGroup(props: ChoiceRadioGroupProps) {
64
67
  ? { 'aria-labelledby': 'label-' + qItem.linkId }
65
68
  : { 'aria-label': qItem.text ?? 'Unnamed radio group' })}
66
69
  aria-readonly={readOnly && readOnlyVisualStyle === 'readonly'}
70
+ aria-describedby={feedback ? feedbackId : undefined}
67
71
  row={orientation === ChoiceItemOrientation.Horizontal}
68
72
  sx={inputsFlexGrow ? { width: '100%', flexWrap: 'nowrap' } : {}}
69
73
  onChange={(e) => {
@@ -93,7 +97,11 @@ function RadioFormGroup(props: ChoiceRadioGroupProps) {
93
97
  <ClearInputButton buttonShown={!!valueRadio} readOnly={readOnly} onClear={onClear} />
94
98
  </Box>
95
99
 
96
- {feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
100
+ {feedback ? (
101
+ <AccessibleFeedback id={feedbackId}>
102
+ <StyledFeedbackTypography>{feedback}</StyledFeedbackTypography>
103
+ </AccessibleFeedback>
104
+ ) : null}
97
105
  </>
98
106
  );
99
107
  }
@@ -27,8 +27,8 @@ import type {
27
27
  PropsWithRenderingExtensionsAttribute
28
28
  } from '../../../interfaces/renderProps.interface';
29
29
  import { useRendererConfigStore } from '../../../stores';
30
- import { StyledRequiredTypography } from '../Item.styles';
31
30
  import DisplayUnitText from '../ItemParts/DisplayUnitText';
31
+ import AccessibleFeedback from '../ItemParts/AccessibleFeedback';
32
32
 
33
33
  interface OpenChoiceSelectAnswerOptionFieldProps
34
34
  extends PropsWithIsTabledAttribute,
@@ -83,6 +83,7 @@ function OpenChoiceSelectAnswerOptionField(props: OpenChoiceSelectAnswerOptionFi
83
83
  textFieldWidth={textFieldWidth}
84
84
  isTabled={isTabled}
85
85
  placeholder={entryFormat || displayPrompt}
86
+ helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
86
87
  {...params}
87
88
  slotProps={{
88
89
  input: {
@@ -99,8 +100,6 @@ function OpenChoiceSelectAnswerOptionField(props: OpenChoiceSelectAnswerOptionFi
99
100
  />
100
101
  )}
101
102
  />
102
-
103
- {feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
104
103
  </>
105
104
  );
106
105
  }
@@ -28,8 +28,8 @@ import type {
28
28
  import type { Coding, QuestionnaireItem } from 'fhir/r4';
29
29
  import type { TerminologyError } from '../../../hooks/useValueSetCodings';
30
30
  import { useRendererConfigStore } from '../../../stores';
31
- import { StyledRequiredTypography } from '../Item.styles';
32
31
  import DisplayUnitText from '../ItemParts/DisplayUnitText';
32
+ import AccessibleFeedback from '../ItemParts/AccessibleFeedback';
33
33
 
34
34
  interface OpenChoiceSelectAnswerValueSetFieldProps
35
35
  extends PropsWithIsTabledAttribute,
@@ -88,6 +88,7 @@ function OpenChoiceSelectAnswerValueSetField(props: OpenChoiceSelectAnswerValueS
88
88
  textFieldWidth={textFieldWidth}
89
89
  isTabled={isTabled}
90
90
  placeholder={entryFormat || displayPrompt}
91
+ helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
91
92
  {...params}
92
93
  slotProps={{
93
94
  input: {
@@ -110,8 +111,6 @@ function OpenChoiceSelectAnswerValueSetField(props: OpenChoiceSelectAnswerValueS
110
111
  {terminologyError.answerValueSet}
111
112
  </Typography>
112
113
  ) : null}
113
-
114
- {feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
115
114
  </>
116
115
  );
117
116
  }
@@ -22,6 +22,7 @@ import DisplayUnitText from '../ItemParts/DisplayUnitText';
22
22
  import { ClearButtonAdornment } from '../ItemParts/ClearButtonAdornment';
23
23
  import ExpressionUpdateFadingIcon from '../ItemParts/ExpressionUpdateFadingIcon';
24
24
  import { StandardTextField } from '../Textfield.styles';
25
+ import AccessibleFeedback from '../ItemParts/AccessibleFeedback';
25
26
 
26
27
  interface QuantityFieldProps extends PropsWithIsTabledAttribute {
27
28
  linkId: string;
@@ -104,7 +105,7 @@ function QuantityField(props: QuantityFieldProps) {
104
105
  )
105
106
  }
106
107
  }}
107
- helperText={feedback}
108
+ helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
108
109
  data-test="q-item-quantity-field"
109
110
  />
110
111
  );
@@ -22,8 +22,9 @@ import Stack from '@mui/material/Stack';
22
22
  import SliderLabels from './SliderLabels';
23
23
  import SliderDisplayValue from './SliderDisplayValue';
24
24
  import { useRendererConfigStore } from '../../../stores';
25
- import { StyledRequiredTypography } from '../Item.styles';
25
+ import { StyledFeedbackTypography } from '../Item.styles';
26
26
  import { StandardSlider } from './Slider.styles';
27
+ import AccessibleFeedback from '../ItemParts/AccessibleFeedback';
27
28
 
28
29
  interface SliderFieldProps extends PropsWithIsTabledAttribute {
29
30
  linkId: string;
@@ -69,6 +70,8 @@ function SliderField(props: SliderFieldProps) {
69
70
 
70
71
  const hasLabels = !!(minLabel || maxLabel);
71
72
 
73
+ const feedbackId = itemType + '-' + linkId + '-feedback';
74
+
72
75
  return (
73
76
  <>
74
77
  <Stack sx={{ ...sliderSx }}>
@@ -95,12 +98,17 @@ function SliderField(props: SliderFieldProps) {
95
98
  disabled={readOnly && readOnlyVisualStyle === 'disabled'}
96
99
  readOnly={readOnly && readOnlyVisualStyle === 'readonly'}
97
100
  aria-readonly={readOnly && readOnlyVisualStyle === 'readonly'}
101
+ aria-describedby={feedback ? feedbackId : undefined}
98
102
  valueLabelDisplay="auto"
99
103
  data-test="q-item-slider-field"
100
104
  />
101
105
  </Stack>
102
106
 
103
- {feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
107
+ {feedback ? (
108
+ <AccessibleFeedback id={feedbackId}>
109
+ <StyledFeedbackTypography>{feedback}</StyledFeedbackTypography>
110
+ </AccessibleFeedback>
111
+ ) : null}
104
112
  </>
105
113
  );
106
114
  }
@@ -25,6 +25,7 @@ import ItemRepopulateButton from '../ItemParts/ItemRepopulateButton';
25
25
  import type { RenderingExtensions } from '../../../hooks/useRenderingExtensions';
26
26
  import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
27
27
  import { StandardTextField } from '../Textfield.styles';
28
+ import AccessibleFeedback from '../ItemParts/AccessibleFeedback';
28
29
 
29
30
  interface StringFieldProps extends PropsWithIsTabledAttribute {
30
31
  qItem: QuestionnaireItem;
@@ -90,7 +91,7 @@ function StringField(props: StringFieldProps) {
90
91
  )
91
92
  }
92
93
  }}
93
- helperText={feedback}
94
+ helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
94
95
  data-test="q-item-string-field"
95
96
  />
96
97
  );
@@ -23,6 +23,7 @@ import ExpressionUpdateFadingIcon from '../ItemParts/ExpressionUpdateFadingIcon'
23
23
  import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
24
24
  import type { RenderingExtensions } from '../../../hooks/useRenderingExtensions';
25
25
  import ItemRepopulateButton from '../ItemParts/ItemRepopulateButton';
26
+ import AccessibleFeedback from '../ItemParts/AccessibleFeedback';
26
27
 
27
28
  interface TextFieldProps {
28
29
  qItem: QuestionnaireItem;
@@ -79,7 +80,7 @@ function TextField(props: TextFieldProps) {
79
80
  )
80
81
  }
81
82
  }}
82
- helperText={feedback}
83
+ helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
83
84
  data-test="q-item-text-field"
84
85
  />
85
86
  );
@@ -22,6 +22,7 @@ import { StandardTextField } from '../Textfield.styles';
22
22
  import { useRendererConfigStore } from '../../../stores';
23
23
  import DisplayUnitText from '../ItemParts/DisplayUnitText';
24
24
  import { ClearButtonAdornment } from '../ItemParts/ClearButtonAdornment';
25
+ import AccessibleFeedback from '../ItemParts/AccessibleFeedback';
25
26
 
26
27
  interface UrlFieldProps extends PropsWithIsTabledAttribute {
27
28
  linkId: string;
@@ -81,7 +82,7 @@ function UrlField(props: UrlFieldProps) {
81
82
  )
82
83
  }
83
84
  }}
84
- helperText={feedback}
85
+ helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
85
86
  data-test="q-item-url-field"
86
87
  />
87
88
  );
@@ -25,6 +25,8 @@ import type { QuestionnaireItem } from 'fhir/r4';
25
25
  import ContextDisplayItem from '../FormComponents/ItemParts/ContextDisplayItem';
26
26
  import { useFocusTabHeading } from '../../hooks/useFocusTabHeading';
27
27
  import useDisplayCqfAndCalculatedExpression from '../../hooks/useDisplayCqfAndCalculatedExpression';
28
+ import RequiredAsterisk from '../FormComponents/ItemParts/RequiredAsterisk';
29
+ import useRenderingExtensions from '../../hooks/useRenderingExtensions';
28
30
 
29
31
  interface FormBodySingleTabProps {
30
32
  qItem: QuestionnaireItem;
@@ -40,6 +42,10 @@ const FormBodySingleTab = memo(function FormBodySingleTab(props: FormBodySingleT
40
42
  const switchTab = useQuestionnaireStore.use.switchTab();
41
43
  const disableHeadingFocusOnTabSwitch =
42
44
  useRendererConfigStore.use.disableHeadingFocusOnTabSwitch();
45
+ const requiredIndicatorPosition = useRendererConfigStore.use.requiredIndicatorPosition();
46
+
47
+ const { required } = useRenderingExtensions(qItem);
48
+ const readOnly = qItem.readOnly ?? false;
43
49
 
44
50
  const focusHeading = useFocusTabHeading();
45
51
 
@@ -68,14 +74,28 @@ const FormBodySingleTab = memo(function FormBodySingleTab(props: FormBodySingleT
68
74
  <ListItemText
69
75
  primary={
70
76
  <Box display="flex" alignItems="center" justifyContent="space-between">
71
- <Typography
72
- id={`tab-${listIndex}`}
73
- component="span"
74
- fontWeight={600}
75
- fontSize="0.8125rem"
76
- aria-label={itemTextAriaLabel}>
77
- {tabLabel}
78
- </Typography>
77
+ <Box display="flex" gap={0.25}>
78
+ {/* Required asterisk position is in front of text */}
79
+ {required && requiredIndicatorPosition === 'start' ? (
80
+ <RequiredAsterisk>*</RequiredAsterisk>
81
+ ) : null}
82
+ <Typography
83
+ id={`tab-${listIndex}`}
84
+ component="span"
85
+ fontWeight={600}
86
+ fontSize="0.8125rem"
87
+ aria-label={itemTextAriaLabel}>
88
+ {tabLabel}
89
+ </Typography>
90
+
91
+ {/* Required asterisk position is behind text */}
92
+ {required && requiredIndicatorPosition === 'end' ? (
93
+ <RequiredAsterisk readOnly={readOnly} variant="groupHeading">
94
+ *
95
+ </RequiredAsterisk>
96
+ ) : null}
97
+ </Box>
98
+
79
99
  <Box display="flex" minHeight={24} minWidth={24} ml={1}>
80
100
  {contextDisplayItems.map((item) => {
81
101
  return <ContextDisplayItem key={item.linkId} displayItem={item} />;