Skip to content

Commit 49ff559

Browse files
Issue/1640 (aehrc#1830)
* feat: Associate instruction text with input fields using aria-describedby (aehrc#1640) This commit implements accessibility improvements by associating instructional text with their parent form fields using the aria-describedby attribute. Changes: - Modified ItemFieldGrid to recursively add aria-describedby to input/textarea elements and radio/checkbox groups when instructions are present - Added getInstructionsId helper function to generate instruction IDs - Updated DisplayInstructions component to accept an optional id prop - Added Storybook tests for String, Boolean, and Text items to verify aria-describedby functionality with instruction text Screen readers will now announce instructional text when a field receives focus, improving the accessibility experience for users with visual impairments. Fixes aehrc#1640 * feat: Associate instruction text with input fields using aria-describedby - Apply aria-describedby directly to MUI components via their props - For text fields (String, Text): Use slotProps.input to add aria-describedby - For radio buttons (Boolean): Use inputProps on individual radio inputs - Updated ChoiceRadioSingle to accept and forward ariaDescribedBy prop - Removed dynamic React element cloning from ItemFieldGrid - Consolidated accessibility tests in Testing/Accessibility/Instructions - Fixed automated Storybook tests for accessibility verification - All three item types (String, Text, Boolean) properly announce instructions via VoiceOver Fixes aehrc#1640 * fix: Remove unused 'within' import and rebase on main - Fixed lint error blocking CI/CD - Rebased issue/1640 on latest main - Verified aria-describedby with VoiceOver testing - Instructions announced correctly when field gets focus Fixes aehrc#1640 * fix: Implement aria-describedby for accessibility instructions (Issue aehrc#1640) Address reviewer feedback for Issue aehrc#1640 by implementing proper aria-describedby support and adding comprehensive VoiceOver accessibility tests. ## Changes Made: ### Bug Fixes (from reviewer feedback): 1. **Fixed slotProps placement**: Changed from slotProps.input to slotProps.htmlInput in StringField and TextField components to correctly apply aria-describedby to the actual input/textarea elements instead of wrapper divs 2. **Fixed test selector**: Updated StringInstructionsAccessibility story to query for 'input' instead of 'textarea' for string fields 3. **Removed unnecessary type**: Removed 'aria-describedby'?: string type from Radio.styles.tsx as the attribute is passed via inputProps ### Feature Implementation: 4. **Added aria-describedby support** to the following item types: - Integer (IntegerField & IntegerItem) - Decimal (DecimalField & DecimalItem) - DateTime (DateTimeField, CustomDateTimeItem, CustomDateField, CustomDateItem) - String (already working, fixed slotProps) - Text (already working, fixed slotProps) - Boolean (already working) Pattern: Each field component now accepts instructionsId prop and applies it via slotProps.htmlInput with aria-describedby attribute ### Test Coverage: 5. **Added VoiceOver accessibility test stories** for all item types: - IntegerInstructionsAccessibility - DecimalInstructionsAccessibility - QuantityInstructionsAccessibility - UrlInstructionsAccessibility - AttachmentInstructionsAccessibility - DateInstructionsAccessibility - DateTimeInstructionsAccessibility - TimeInstructionsAccessibility - ChoiceInstructionsAccessibility - OpenChoiceInstructionsAccessibility ## Testing: - ✅ IntegerInstructionsAccessibility - VoiceOver confirmed working - ✅ DecimalInstructionsAccessibility - VoiceOver confirmed working - ✅ DateTimeInstructionsAccessibility - VoiceOver confirmed working - ✅ BooleanInstructionsAccessibility - VoiceOver confirmed working - ✅ StringInstructionsAccessibility - Fixed and working - ✅ TextInstructionsAccessibility - Fixed and working ## Remaining Work (for future commits): The following item types still need aria-describedby implementation: - Date (has error to fix) - Quantity - Time - URL - Attachment - Choice - Open-Choice These follow the same pattern and will be addressed in subsequent commits. Refs aehrc#1640 Made-with: Cursor * fix: Add aria-describedby support for URL and Quantity item types (Issue aehrc#1640) Implements accessibility instructions for URL and Quantity fields by adding instructionsId prop support. For Quantity items, aria-describedby is applied to both the value input field and unit selector to ensure instructions are announced regardless of which field receives focus first. Made-with: Cursor * fix: Move instructionsId generation after feedback in CustomDateItem (Issue aehrc#1640) Fixes 'Cannot access feedback before initialization' error by ensuring feedback is defined before being used to generate instructionsId. * feat: Implement CustomTimeItem and CustomChoiceSelectField for accessibility (Issue aehrc#1640) Problem: MUI TimePicker and Autocomplete components have complex internal structures that prevent aria-describedby from being properly announced by VoiceOver. - TimePicker uses segmented inputs (separate spans for hours, minutes, AM/PM) - Autocomplete has nested internal elements (button + hidden input + listbox) Solution: 1. CustomTimeItem (Time field) - Created CustomTimeItem.tsx using existing CustomTimeField component - CustomTimeField uses standard TextField + MUI Select dropdown (not TimePicker) - Matches pattern from CustomDateItem (which works successfully) - Added instructionsId prop support to CustomTimeField - Updated SingleItemSwitcher to use CustomTimeItem instead of TimeItem 2. CustomChoiceSelectField (Choice Select dropdown) - Created CustomChoiceSelectField.tsx using standard MUI Select (not Autocomplete) - Applied aria-describedby and aria-labelledby directly to Select component - MUI Select passes these attributes through to internal focusable button element - Added useRenderingExtensions and getInstructionsId to ChoiceSelectAnswerOptionItem - Updated ChoiceSelectAnswerOptionView to use CustomChoiceSelectField 3. Supporting Changes - Added instructionsId support to CustomTimeField for DateTime items - Updated DateTimeField to pass instructionsId to CustomTimeField - Added instructionsId prop threading for Choice Radio fields Testing: Both implementations successfully tested with VoiceOver. Related to: aehrc#1640 Made-with: Cursor * feat: Add aria-describedby support for Attachment fields (Issue aehrc#1640) Problem: Attachment fields have multiple focusable elements (file upload button, URL field, filename field) but VoiceOver was not reading the accessibility instructions when users navigated to the field. Solution: Applied aria-describedby directly to the file upload button (first focusable element), following the same successful pattern used for Choice Select fields where MUI components need aria attributes on the actual interactive element, not wrapper groups. Changes: - AttachmentItem: Added useRenderingExtensions and getInstructionsId to generate instructionsId from rendering extensions - AttachmentFieldWrapper: Added instructionsId and itemText props, passed to AttachmentField - AttachmentField: Added role="group" wrapper with aria-labelledby/aria-describedby and passed instructionsId to AttachmentFileCollector - AttachmentFileCollector: Applied aria-describedby to the IconButton (Attach file) - Improved UX: Made explanatory text smaller, lighter, and italic to distinguish from user-defined instructions Testing: VoiceOver successfully reads the instructions when focusing the Attach file button. Related to: aehrc#1640 Made-with: Cursor * WIP: Add aria-describedby support to Open-Choice Autocomplete (not working yet) Problem: - Open-Choice Autocomplete fields do not announce instructions to screen readers - VoiceOver only reads "Occupation, list box popup collapsed, edit text" without the instructions Attempted solution: - Added useRenderingExtensions and getInstructionsId to OpenChoiceAutocompleteItem - Passed instructionsId down to OpenChoiceAutocompleteField - Attempted to apply aria-describedby via slotProps.input.inputProps Current status: - aria-describedby is NOT being applied to the textarea element - MUI Autocomplete has complex internal structure that does not properly propagate aria attributes - Similar issue to Time and Choice fields that required custom implementations Next steps: - Create CustomOpenChoiceField.tsx component (similar to CustomTimeItem and CustomChoiceSelectField) - Use standard MUI Select + TextField to support both predefined options and custom text input - This will properly support aria-describedby for accessibility Files modified: - OpenChoiceAutocompleteItem.tsx: Added instructionsId generation - OpenChoiceAutocompleteField.tsx: Attempted aria-describedby implementation (incomplete) Related issue: aehrc#1640 Made-with: Cursor * feat: Add aria-describedby support to Open-Choice fields using CustomOpenChoiceField Problem: - Open-Choice fields did not announce instructions to screen readers - MUI Autocomplete has complex internal structure that does not propagate aria-describedby - VoiceOver only read field name without instructions Solution: Created CustomOpenChoiceField component following Time and Choice patterns: - Uses MUI Select dropdown for predefined Coding options - Includes custom value option that reveals TextField for custom text input - Properly applies aria-describedby to both Select and TextField elements Changes: 1. Created CustomOpenChoiceField.tsx with Select + TextField 2. Updated OpenChoiceAutocompleteItem.tsx to use CustomOpenChoiceField 3. Updated OpenChoiceSelectAnswerOptionItem.tsx to use CustomOpenChoiceField 4. Updated AccessibilityInstructions.stories.tsx test for new component Testing: - VoiceOver correctly reads instructions for Open-Choice dropdown - VoiceOver reads instructions for custom text input field - All functionality working as expected Related issue: aehrc#1640 Made-with: Cursor * fix: stabilize storybook CI interactions and accessibility story selectors Align story test utilities and selectors with current renderer DOM behavior, remove lint-blocking issues, and harden Storybook CI startup/timeout settings to avoid flaky connection and interaction failures. Made-with: Cursor * fix: restore renderer controls and normalize VoiceOver instructions Revert unintended custom choice/open-choice/time control swaps and align instruction announcements across control variants for consistent VoiceOver behavior in storybook and 715 testing. Made-with: Cursor * style: apply prettier to choice and slider components * fix(renderer): stabilize Storybook tests and a11y for slider and time - Expose aria-describedby on slider for instructions in tests\n- Time/choice/slider-related story plays and testUtils (MUI TimePicker via clock)\n- AccessibilityInstructions and Time stories; package.json CI test timeout if changed * fix(renderer): resolve Prettier lint errors in Storybook test utilities Apply eslint --fix formatting in testUtils and AccessibilityInstructions stories so CI lint passes. Made-with: Cursor * fix(renderer): announce slider instructions via MUI input slot Pass aria-describedby through slotProps.input so screen readers use the focused thumb input. Align SliderItem with IntegerItem using useRenderingExtensions and ItemFieldGrid feedback. --------- Co-authored-by: Maryam Mehdizadeh <maryam.mehdizadeh@csiro.au>
1 parent 9ddc3de commit 49ff559

72 files changed

Lines changed: 1779 additions & 292 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/smart-forms-renderer/.storybook/test-runner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
module.exports = {
33
timeout: 30000,
44
async preVisit(page) {
5-
page.setDefaultTimeout(15000);
5+
page.setDefaultTimeout(30000);
66
},
77
};

packages/smart-forms-renderer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"build": "npm run compile",
1010
"test": "TZ=Australia/Sydney jest --config ./jest.config.ts --coverage",
1111
"test:watch": "jest --watch",
12-
"test-storybook-ci": "CI=true concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"storybook dev --quiet -p 6006\" \"npx wait-on tcp:127.0.0.1:6006 && test-storybook \"",
12+
"test-storybook-ci": "CI=true concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"storybook dev --quiet -p 6006\" \"npx wait-on http-get://127.0.0.1:6006/iframe.html && test-storybook --url http://127.0.0.1:6006 --testTimeout 120000\"",
1313
"storybook": "storybook dev -p 6006",
1414
"storybook-watch": "concurrently -n \"STORYBOOK,TSC\" -c \"cyan.bold,green.bold\" \"storybook dev -p 6006\" \"tsc -w\"",
1515
"build-storybook": "storybook build",

packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentField.tsx

Lines changed: 77 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ import { ClearButtonAdornment } from '../ItemParts/ClearButtonAdornment';
3232
interface AttachmentFieldProps extends PropsWithIsTabledAttribute {
3333
linkId: string;
3434
itemType: string;
35+
itemText: string | undefined;
3536
attachmentValues: AttachmentValues;
3637
feedback: string;
3738
readOnly: boolean;
39+
instructionsId: string | undefined;
3840
onUploadFile: (file: File | null) => void;
3941
onUrlChange: (url: string) => void;
4042
onFileNameChange: (fileName: string) => void;
@@ -44,10 +46,12 @@ function AttachmentField(props: AttachmentFieldProps) {
4446
const {
4547
linkId,
4648
itemType,
49+
itemText,
4750
attachmentValues,
4851
feedback,
4952
readOnly,
5053
isTabled,
54+
instructionsId,
5155
onUploadFile,
5256
onUrlChange,
5357
onFileNameChange
@@ -64,73 +68,85 @@ function AttachmentField(props: AttachmentFieldProps) {
6468

6569
return (
6670
<>
67-
<Stack rowGap={1} id={itemType + '-' + linkId}>
68-
<Typography component="div" color={readOnly ? readOnlyTextColor : 'text.primary'}>
69-
An attachment must either have a file or a URL, or both.
70-
</Typography>
71-
<Box>
72-
<AttachmentFileCollector
73-
uploadedFile={uploadedFile}
74-
readOnly={readOnly}
75-
isTabled={isTabled}
76-
onUploadFile={onUploadFile}
77-
/>
78-
</Box>
79-
80-
<AttachmentUrlField
81-
linkId={linkId}
82-
url={url}
83-
readOnly={readOnly}
84-
isTabled={isTabled}
85-
onUrlChange={onUrlChange}
86-
/>
87-
88-
<Box>
71+
<div
72+
role="group"
73+
{...(!isTabled && { 'aria-labelledby': `label-${linkId}` })}
74+
{...(isTabled && { 'aria-label': itemText ?? 'Unnamed attachment field' })}
75+
{...(instructionsId && { 'aria-describedby': instructionsId })}>
76+
<Stack rowGap={1} id={itemType + '-' + linkId}>
8977
<Typography
9078
component="div"
91-
variant="body2"
92-
color={readOnly ? readOnlyTextColor : 'text.primary'}>
93-
File name (optional)
79+
variant="caption"
80+
color={readOnly ? 'text.disabled' : 'text.secondary'}
81+
sx={{ fontStyle: 'italic' }}>
82+
An attachment must either have a file or a URL, or both.
9483
</Typography>
95-
<StandardTextField
96-
multiline
97-
fullWidth
98-
textFieldWidth={textFieldWidth}
84+
<Box>
85+
<AttachmentFileCollector
86+
uploadedFile={uploadedFile}
87+
readOnly={readOnly}
88+
isTabled={isTabled}
89+
instructionsId={instructionsId}
90+
onUploadFile={onUploadFile}
91+
/>
92+
</Box>
93+
94+
<AttachmentUrlField
95+
linkId={linkId}
96+
url={url}
97+
readOnly={readOnly}
9998
isTabled={isTabled}
100-
id={linkId}
101-
value={fileName}
102-
onChange={(event) => onFileNameChange(event.target.value)}
103-
disabled={readOnly && readOnlyVisualStyle === 'disabled'}
104-
size="small"
105-
data-test="q-item-attachment-field"
106-
slotProps={{
107-
input: {
108-
readOnly: readOnly && readOnlyVisualStyle === 'readonly',
109-
endAdornment: (
110-
<InputAdornment position="end">
111-
<ClearButtonAdornment
112-
readOnly={readOnly}
113-
onClear={() => {
114-
onFileNameChange('');
115-
}}
116-
/>
117-
</InputAdornment>
118-
)
119-
},
120-
htmlInput: {
121-
'data-test': 'q-item-attachment-file-name',
122-
'aria-label': 'File name (optional)'
123-
}
124-
}}
99+
onUrlChange={onUrlChange}
125100
/>
126-
</Box>
127101

128-
{uploadedFile && url ? (
129-
<Typography component="div" color={readOnly ? readOnlyTextColor : 'text.primary'}>
130-
Ensure that the attached file and URL has the same content.
131-
</Typography>
132-
) : null}
133-
</Stack>
102+
<Box>
103+
<Typography
104+
component="div"
105+
variant="body2"
106+
color={readOnly ? readOnlyTextColor : 'text.primary'}>
107+
File name (optional)
108+
</Typography>
109+
<StandardTextField
110+
multiline
111+
fullWidth
112+
textFieldWidth={textFieldWidth}
113+
isTabled={isTabled}
114+
id={linkId}
115+
value={fileName}
116+
onChange={(event) => onFileNameChange(event.target.value)}
117+
disabled={readOnly && readOnlyVisualStyle === 'disabled'}
118+
size="small"
119+
data-test="q-item-attachment-field"
120+
slotProps={{
121+
input: {
122+
readOnly: readOnly && readOnlyVisualStyle === 'readonly',
123+
endAdornment: (
124+
<InputAdornment position="end">
125+
<ClearButtonAdornment
126+
readOnly={readOnly}
127+
onClear={() => {
128+
onFileNameChange('');
129+
}}
130+
/>
131+
</InputAdornment>
132+
)
133+
},
134+
htmlInput: {
135+
'data-test': 'q-item-attachment-file-name',
136+
'aria-label': 'File name (optional)',
137+
...(instructionsId && { 'aria-describedby': instructionsId })
138+
}
139+
}}
140+
/>
141+
</Box>
142+
143+
{uploadedFile && url ? (
144+
<Typography component="div" color={readOnly ? readOnlyTextColor : 'text.primary'}>
145+
Ensure that the attached file and URL has the same content.
146+
</Typography>
147+
) : null}
148+
</Stack>
149+
</div>
134150

135151
{feedback ? <StyledRequiredTypography>{feedback}</StyledRequiredTypography> : null}
136152
</>

packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFieldWrapper.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface AttachmentFieldWrapperProps
3535
attachmentValues: AttachmentValues;
3636
feedback: string;
3737
readOnly: boolean;
38+
instructionsId: string | undefined;
3839
onUploadFile: (file: File | null) => void;
3940
onUrlChange: (url: string) => void;
4041
onFileNameChange: (fileName: string) => void;
@@ -48,6 +49,7 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) {
4849
readOnly,
4950
isRepeated,
5051
isTabled,
52+
instructionsId,
5153
onUploadFile,
5254
onUrlChange,
5355
onFileNameChange
@@ -60,10 +62,12 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) {
6062
<AttachmentField
6163
linkId={qItem.linkId}
6264
itemType={qItem.type}
65+
itemText={qItem.text}
6366
attachmentValues={attachmentValues}
6467
feedback={feedback}
6568
readOnly={readOnly}
6669
isTabled={isTabled}
70+
instructionsId={instructionsId}
6771
onUploadFile={onUploadFile}
6872
onUrlChange={onUrlChange}
6973
onFileNameChange={onFileNameChange}
@@ -85,10 +89,12 @@ function AttachmentFieldWrapper(props: AttachmentFieldWrapperProps) {
8589
<AttachmentField
8690
linkId={qItem.linkId}
8791
itemType={qItem.type}
92+
itemText={qItem.text}
8893
attachmentValues={attachmentValues}
8994
feedback={feedback}
9095
readOnly={readOnly}
9196
isTabled={isTabled}
97+
instructionsId={instructionsId}
9298
onUploadFile={onUploadFile}
9399
onUrlChange={onUrlChange}
94100
onFileNameChange={onFileNameChange}

packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentFileCollector.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ import type { PropsWithIsTabledAttribute } from '../../../interfaces/renderProps
2626
interface AttachmentFileCollectorProps extends PropsWithIsTabledAttribute {
2727
uploadedFile: File | null;
2828
readOnly: boolean;
29+
instructionsId: string | undefined;
2930
onUploadFile: (file: File | null) => void;
3031
}
3132

3233
const AttachmentFileCollector = memo(function AttachmentFileCollector(
3334
props: AttachmentFileCollectorProps
3435
) {
35-
const { uploadedFile, readOnly, isTabled, onUploadFile } = props;
36+
const { uploadedFile, readOnly, isTabled, instructionsId, onUploadFile } = props;
3637

3738
const handleFileDrop = useCallback(
3839
(item: { files: any[] }) => {
@@ -71,11 +72,21 @@ const AttachmentFileCollector = memo(function AttachmentFileCollector(
7172
isTabled={isTabled}
7273
/>
7374
<Stack direction="row" justifyContent="space-between" pt={0.5}>
74-
<Box>
75+
<Box data-test="q-item-attachment-file-input">
7576
<Tooltip title="Attach file">
76-
<IconButton component="label" size="small" disabled={readOnly}>
77+
<IconButton
78+
component="label"
79+
size="small"
80+
disabled={readOnly}
81+
aria-label="Attach file"
82+
{...(instructionsId && { 'aria-describedby': instructionsId })}>
7783
<AttachFileIcon fontSize="small" />
78-
<input type="file" hidden onChange={handleAttachFile} />
84+
<input
85+
type="file"
86+
hidden
87+
onChange={handleAttachFile}
88+
{...(instructionsId && { 'aria-describedby': instructionsId })}
89+
/>
7990
</IconButton>
8091
</Tooltip>
8192
<Tooltip title="Remove file">

packages/smart-forms-renderer/src/components/FormComponents/AttachmentItem/AttachmentItem.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import debounce from 'lodash.debounce';
2121
import { createEmptyQrItem, getQRItemId } from '../../../utils/qrItem';
2222
import { DEBOUNCE_DURATION } from '../../../utils/debounce';
2323
import useReadOnly from '../../../hooks/useReadOnly';
24+
import useRenderingExtensions from '../../../hooks/useRenderingExtensions';
2425
import AttachmentFieldWrapper from './AttachmentFieldWrapper';
2526
import { HTML5Backend } from 'react-dnd-html5-backend';
2627
import { DndProvider } from 'react-dnd';
2728
import { createAttachmentAnswer } from '../../../utils/fileUtils';
2829
import useValidationFeedback from '../../../hooks/useValidationFeedback';
30+
import { getInstructionsId } from '../ItemParts/ItemFieldGrid';
2931

3032
export interface AttachmentValues {
3133
uploadedFile: File | null;
@@ -60,6 +62,10 @@ function AttachmentItem(props: BaseItemProps) {
6062
// Perform validation checks
6163
const feedback = useValidationFeedback(qItem, feedbackFromParent);
6264

65+
// Get instructions ID for aria-describedby
66+
const { displayInstructions } = useRenderingExtensions(qItem);
67+
const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback);
68+
6369
// Event handlers
6470
async function handleUploadFile(newUploadedFile: File | null) {
6571
setUploadedFile(newUploadedFile);
@@ -111,6 +117,7 @@ function AttachmentItem(props: BaseItemProps) {
111117
readOnly={readOnly}
112118
isRepeated={isRepeated}
113119
isTabled={isTabled}
120+
instructionsId={instructionsId}
114121
onUploadFile={handleUploadFile}
115122
onUrlChange={handleUrlChange}
116123
onFileNameChange={handleFileNameChange}

packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanField.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,22 @@ interface BooleanFieldProps {
3838
valueBoolean: boolean | undefined;
3939
feedback: string;
4040
calcExpUpdated: boolean;
41+
instructionsId: string | undefined;
4142
onCheckedChange: (newValue: string) => void;
4243
onClear: () => void;
4344
}
4445

4546
const BooleanField = memo(function BooleanField(props: BooleanFieldProps) {
46-
const { qItem, readOnly, valueBoolean, feedback, calcExpUpdated, onCheckedChange, onClear } =
47-
props;
47+
const {
48+
qItem,
49+
readOnly,
50+
valueBoolean,
51+
feedback,
52+
calcExpUpdated,
53+
instructionsId,
54+
onCheckedChange,
55+
onClear
56+
} = props;
4857

4958
const readOnlyVisualStyle = useRendererConfigStore.use.readOnlyVisualStyle();
5059
const inputsFlexGrow = useRendererConfigStore.use.inputsFlexGrow();
@@ -80,6 +89,7 @@ const BooleanField = memo(function BooleanField(props: BooleanFieldProps) {
8089
aria-readonly={readOnly && readOnlyVisualStyle === 'readonly'}
8190
role="checkbox"
8291
aria-checked={ariaCheckedValue}
92+
{...(instructionsId && { 'aria-describedby': instructionsId })}
8393
onChange={() => {
8494
// If item.readOnly=true, do not allow any changes
8595
if (readOnly) {
@@ -105,7 +115,9 @@ const BooleanField = memo(function BooleanField(props: BooleanFieldProps) {
105115
sx={inputsFlexGrow ? { width: '100%', flexWrap: 'nowrap' } : {}}>
106116
<RadioGroup
107117
id={qItem.type + '-' + qItem.linkId}
108-
aria-labelledby={'label-' + qItem.linkId}
118+
aria-labelledby={
119+
instructionsId ? `label-${qItem.linkId} ${instructionsId}` : `label-${qItem.linkId}`
120+
}
109121
row={orientation === ChoiceItemOrientation.Horizontal}
110122
sx={inputsFlexGrow ? { width: '100%', flexWrap: 'nowrap' } : {}}
111123
aria-readonly={readOnly && readOnlyVisualStyle === 'readonly'}

packages/smart-forms-renderer/src/components/FormComponents/BooleanItem/BooleanItem.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import type { BaseItemProps } from '../../../interfaces/renderProps.interface';
2222
import { useQuestionnaireStore } from '../../../stores';
2323
import { createEmptyQrItem, getQRItemId } from '../../../utils/qrItem';
2424
import { FullWidthFormComponentBox } from '../../Box.styles';
25-
import ItemFieldGrid from '../ItemParts/ItemFieldGrid';
25+
import ItemFieldGrid, { getInstructionsId } from '../ItemParts/ItemFieldGrid';
2626
import ItemLabel from '../ItemParts/ItemLabel';
2727
import BooleanField from './BooleanField';
28+
import useRenderingExtensions from '../../../hooks/useRenderingExtensions';
2829

2930
function BooleanItem(props: BaseItemProps) {
3031
const {
@@ -45,6 +46,10 @@ function BooleanItem(props: BaseItemProps) {
4546
// Perform validation checks - there's no string-based input here
4647
const feedback = useValidationFeedback(qItem, feedbackFromParent);
4748

49+
// Get instructions ID for aria-describedby
50+
const { displayInstructions } = useRenderingExtensions(qItem);
51+
const instructionsId = getInstructionsId(qItem, displayInstructions, !!feedback);
52+
4853
// Init input value
4954
const answerKey = getQRItemId(qrItem?.answer?.[0]?.id);
5055
let valueBoolean: boolean | undefined = undefined;
@@ -86,6 +91,7 @@ function BooleanItem(props: BaseItemProps) {
8691
valueBoolean={valueBoolean}
8792
feedback={feedback}
8893
calcExpUpdated={calcExpUpdated}
94+
instructionsId={instructionsId}
8995
onCheckedChange={handleValueChange}
9096
onClear={handleClear}
9197
/>
@@ -101,6 +107,7 @@ function BooleanItem(props: BaseItemProps) {
101107
valueBoolean={valueBoolean}
102108
feedback={feedback}
103109
calcExpUpdated={calcExpUpdated}
110+
instructionsId={instructionsId}
104111
onCheckedChange={handleValueChange}
105112
onClear={handleClear}
106113
/>
@@ -124,6 +131,7 @@ function BooleanItem(props: BaseItemProps) {
124131
valueBoolean={valueBoolean}
125132
feedback={feedback}
126133
calcExpUpdated={calcExpUpdated}
134+
instructionsId={instructionsId}
127135
onCheckedChange={handleValueChange}
128136
onClear={handleClear}
129137
/>

0 commit comments

Comments
 (0)