Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added apps/smart-forms-app/vite
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* limitations under the License.
*/

import { useState } from 'react';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import Autocomplete from '@mui/material/Autocomplete';
import Typography from '@mui/material/Typography';
Expand Down Expand Up @@ -66,6 +67,16 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField

const { displayUnit, displayPrompt, entryFormat } = renderingExtensions;

const [open, setOpen] = useState(false);

// Handle focus with delayed dropdown opening for better screen reader experience
function handleFocus() {
// Delay opening to allow screen readers to announce the field name first
setTimeout(() => {
setOpen(true);
}, 150); // 150ms delay allows VoiceOver to announce the field
}

if (codings.length > 0) {
return (
<>
Expand All @@ -79,6 +90,9 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField
value={valueCoding ?? null}
onChange={(_, newValue) => onSelectChange(newValue)}
autoHighlight
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
sx={{ maxWidth: !isTabled ? textFieldWidth : 3000, minWidth: 160, flexGrow: 1 }}
size="small"
disabled={readOnly && readOnlyVisualStyle === 'disabled'}
Expand All @@ -89,6 +103,7 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField
textFieldWidth={textFieldWidth}
isTabled={isTabled}
placeholder={entryFormat || displayPrompt}
onFocus={handleFocus}
{...params}
slotProps={{
input: {
Expand All @@ -103,7 +118,9 @@ function ChoiceSelectAnswerValueSetFields(props: ChoiceSelectAnswerValueSetField
),
inputProps: {
...params.inputProps,
'aria-label': qItem.text ?? 'Unnamed choice dropdown'
...(isTabled
? { 'aria-label': qItem.text ?? 'Unnamed choice dropdown' }
: { 'aria-labelledby': `label-${qItem.linkId}` })
}
}
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { PropsWithIsTabledAttribute } from '../../../../interfaces/renderPr
import { useRendererConfigStore } from '../../../../stores';
import FormControl from '@mui/material/FormControl';
import MuiTextField from '../../TextItem/MuiTextField';
import AccessibleFeedback from '../../ItemParts/AccessibleFeedback';

interface CustomTimeFieldProps extends PropsWithIsTabledAttribute {
linkId: string;
Expand Down Expand Up @@ -113,9 +114,11 @@ function CustomTimeField(props: CustomTimeFieldProps) {
</Select>
</FormControl>
</Box>
<Typography component="span" variant="caption" color="error" sx={{ ml: 1.75, mt: -0.5 }}>
{feedback}
</Typography>
<AccessibleFeedback>
<Typography component="span" variant="caption" color="error" sx={{ ml: 1.75, mt: -0.5 }}>
{feedback}
</Typography>
</AccessibleFeedback>
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function DecimalField(props: DecimalFieldProps) {
htmlInput: {
inputMode: 'numeric',
pattern: '[0-9]*',
...(ariaLabel && { 'aria-label': ariaLabel })
'aria-label': ariaLabel
},
input: {
readOnly: readOnly && readOnlyVisualStyle === 'readonly',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
import ItemRepopulateButton from '../ItemParts/ItemRepopulateButton';
import type { RenderingExtensions } from '../../../hooks/useRenderingExtensions';
import { StandardTextField } from '../Textfield.styles';
import AccessibleFeedback from '../ItemParts/AccessibleFeedback';

interface IntegerFieldProps extends PropsWithIsTabledAttribute {
qItem: QuestionnaireItem;
Expand Down Expand Up @@ -74,7 +75,7 @@ function IntegerField(props: IntegerFieldProps) {
id={inputId}
value={input}
error={!!feedback}
helperText={feedback}
helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
onChange={(event) => onInputChange(event.target.value)}
disabled={readOnly && readOnlyVisualStyle === 'disabled'}
label={displayPrompt}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';

export const StyledRequiredTypography = styled(Typography)(({ theme }) => ({
export const StyledFeedbackTypography = styled(Typography)(({ theme }) => ({
color: theme.palette.error.main,
fontSize: '0.75rem',
marginTop: 4
}));

// Legacy alias for backwards compatibility
export const StyledRequiredTypography = StyledFeedbackTypography;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 ReactNode } from 'react';

interface AccessibleFeedbackProps {
children: ReactNode;
id?: string;
}

function AccessibleFeedback(props: AccessibleFeedbackProps) {
const { children, id } = props;

return (
<span id={id} role="alert" aria-live="assertive">
{children}
</span>
);
}

export default AccessibleFeedback;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { StandardTextField } from '../Textfield.styles';
interface QuantityFieldProps extends PropsWithIsTabledAttribute {
linkId: string;
itemType: string;
itemText: string | undefined;
itemText?: string;
input: string;
feedback: string;
displayPrompt: string;
Expand Down Expand Up @@ -83,11 +83,7 @@ function QuantityField(props: QuantityFieldProps) {
isTabled={isTabled}
size="small"
slotProps={{
htmlInput: {
inputMode: 'numeric',
pattern: '[0-9]*',
...(ariaLabel && { 'aria-label': ariaLabel })
},
htmlInput: { inputMode: 'numeric', pattern: '[0-9]*', 'aria-label': ariaLabel },
input: {
readOnly: readOnly && readOnlyVisualStyle === 'readonly',
endAdornment: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import ItemRepopulateButton from '../ItemParts/ItemRepopulateButton';
import type { RenderingExtensions } from '../../../hooks/useRenderingExtensions';
import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
import { StandardTextField } from '../Textfield.styles';
import AccessibleFeedback from '../ItemParts/AccessibleFeedback';

interface StringFieldProps extends PropsWithIsTabledAttribute {
qItem: QuestionnaireItem;
Expand Down Expand Up @@ -90,7 +91,7 @@ function StringField(props: StringFieldProps) {
)
}
}}
helperText={feedback}
helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
data-test="q-item-string-field"
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ExpressionUpdateFadingIcon from '../ItemParts/ExpressionUpdateFadingIcon'
import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
import type { RenderingExtensions } from '../../../hooks/useRenderingExtensions';
import ItemRepopulateButton from '../ItemParts/ItemRepopulateButton';
import AccessibleFeedback from '../ItemParts/AccessibleFeedback';

interface TextFieldProps {
qItem: QuestionnaireItem;
Expand Down Expand Up @@ -79,7 +80,7 @@ function TextField(props: TextFieldProps) {
)
}
}}
helperText={feedback}
helperText={<AccessibleFeedback>{feedback}</AccessibleFeedback>}
data-test="q-item-text-field"
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import {
getInputText,
inputDecimal,
questionnaireFactory,
questionnaireResponseFactory
questionnaireResponseFactory,
unitExtFactory
} from '../testUtils';
import { expect, fireEvent } from 'storybook/test';
import { expect, fireEvent, within } from 'storybook/test';
import { createStory } from '../storybookWrappers/createStory';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
Expand Down Expand Up @@ -79,7 +80,7 @@ export const DecimalBasic: Story = createStory({
expect(result[0]).toEqual(expect.objectContaining({ valueDecimal: targetWeight }));

// Clear value
const clearButton = canvasElement.querySelector('button[title="Clear"]');
const clearButton = canvasElement.querySelector('button[aria-label="Clear"]');
fireEvent.click(clearButton as HTMLElement);

// Here we await for debounced store update
Expand All @@ -104,3 +105,29 @@ export const DecimalBasicResponse: Story = createStory({
expect(input).toBe(targetWeight.toString());
}
}) as Story;
/* Decimal Unit Accessibility story */
const accessibilityTargetLinkId = 'height';
const qDecimalAccessibility = questionnaireFactory([
{
linkId: accessibilityTargetLinkId,
extension: [unitExtFactory('cm', 'cm')],
type: 'decimal',
text: 'Height'
}
]);

export const DecimalUnitAccessibility: Story = createStory({
args: {
questionnaire: qDecimalAccessibility
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Find the decimal input field by its data-test attribute
const inputField = canvas.getByTestId('q-item-decimal-field');
const input = inputField.querySelector('input');

// Verify the aria-label includes the item text and unit for screen reader accessibility
expect(input?.getAttribute('aria-label')).toBe('Height (cm)');
}
}) as Story;
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ import {
getInputText,
inputInteger,
questionnaireFactory,
questionnaireResponseFactory
questionnaireResponseFactory,
unitExtFactory
} from '../testUtils';
import { expect, fireEvent } from 'storybook/test';
import { expect, fireEvent, within } from 'storybook/test';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta = {
Expand Down Expand Up @@ -79,7 +80,7 @@ export const IntegerBasic: Story = createStory({
expect(result[0]).toEqual(expect.objectContaining({ valueInteger: basicAge }));

// Clear value
const clearButton = canvasElement.querySelector('button[title="Clear"]');
const clearButton = canvasElement.querySelector('button[aria-label="Clear"]');
fireEvent.click(clearButton as HTMLElement);

// Here we await for debounced store update
Expand All @@ -103,3 +104,29 @@ export const IntegerBasicResponse: Story = createStory({
expect(input).toBe(targetAge.toString());
}
}) as Story;
/* Integer Unit Accessibility story */
const accessibilityTargetLinkId = 'heart-rate';
const qIntegerAccessibility = questionnaireFactory([
{
linkId: accessibilityTargetLinkId,
extension: [unitExtFactory('bpm', 'beats per minute')],
type: 'integer',
text: 'Heart Rate'
}
]);

export const IntegerUnitAccessibility: Story = createStory({
args: {
questionnaire: qIntegerAccessibility
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Find the integer input field by its data-test attribute
const inputField = canvas.getByTestId('q-item-integer-field');
const input = inputField.querySelector('input');

// Verify the aria-label includes the item text and unit for screen reader accessibility
expect(input?.getAttribute('aria-label')).toBe('Heart Rate (beats per minute)');
}
}) as Story;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, waitFor } from 'storybook/test';
import { expect, waitFor, within } from 'storybook/test';
import BuildFormWrapperForStorybook from '../storybookWrappers/BuildFormWrapperForStorybook';
import {
getAnswers,
Expand Down Expand Up @@ -441,3 +441,29 @@ export const QuantityUnitOptionSingleResponse: Story = createStory({
);
}
}) as Story;
/* Quantity Unit Accessibility story */
const accessibilityTargetLinkId = 'body-height';
const qQuantityAccessibility = questionnaireFactory([
{
linkId: accessibilityTargetLinkId,
extension: [unitExtFactory('cm', 'cm')],
type: 'quantity',
text: 'Height'
}
]);

export const QuantityUnitAccessibility: Story = createStory({
args: {
questionnaire: qQuantityAccessibility
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Find the quantity input field by its data-test attribute
const inputField = canvas.getByTestId('q-item-quantity-field');
const input = inputField.querySelector('input');

// Verify the aria-label includes the item text and unit for screen reader accessibility
expect(input?.getAttribute('aria-label')).toBe('Height (cm)');
}
}) as Story;
Loading
Loading