Skip to content

Commit 76dfcba

Browse files
committed
Render alert elements prior to new content
1 parent 888e9b7 commit 76dfcba

File tree

4 files changed

+70
-57
lines changed

4 files changed

+70
-57
lines changed

packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ export function ConnectedFormGroup<T extends ConnectedField>({
5555
id,
5656
label,
5757
name,
58-
labelSize,
5958
spacing = 'fit',
6059
isSoloField,
6160
infotip,
@@ -82,7 +81,6 @@ export function ConnectedFormGroup<T extends ConnectedField>({
8281
infotip={infotip}
8382
isSoloField={isSoloField}
8483
required={!!validation?.required}
85-
size={labelSize}
8684
>
8785
{label}
8886
</FormGroupLabel>
@@ -103,31 +101,36 @@ export function ConnectedFormGroup<T extends ConnectedField>({
103101
name={name}
104102
/>
105103
{children}
106-
{showError && (
107-
<FormError
108-
aria-live={isFirstError ? 'assertive' : 'off'}
109-
id={errorId}
110-
role={isFirstError ? 'alert' : 'status'}
111-
variant={errorType}
112-
>
104+
{/*
105+
* For screen readers to read new content, role="alert" and/or
106+
* aria-live wrapper elements must be present *before* content is
107+
* added. Thus, we need to render the FormError span always,
108+
* regardless of whether or not there is an error.
109+
*/}
110+
<FormError
111+
aria-live={isFirstError ? 'assertive' : 'off'}
112+
id={errorId}
113+
role={isFirstError ? 'alert' : 'status'}
114+
variant={errorType}
115+
>
116+
{showError && (
113117
<Markdown
114118
inline
115119
overrides={{
116120
a: {
117121
allowedAttributes: ['href', 'target'],
118122
component: ErrorAnchor,
119-
processNode: (
120-
node: unknown,
121-
props: { onClick?: () => void }
122-
) => <ErrorAnchor {...props} />,
123+
processNode: (_: unknown, props: { onClick?: () => void }) => (
124+
<ErrorAnchor {...props} />
125+
),
123126
},
124127
}}
125128
skipDefaultOverrides={{ a: true }}
126129
spacing="none"
127130
text={textError}
128131
/>
129-
</FormError>
130-
)}
132+
)}
133+
</FormError>
131134
</FormGroup>
132135
);
133136
}

packages/gamut/src/Form/elements/FormGroup.tsx

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { variant } from '@codecademy/gamut-styles';
1+
import { css, variant } from '@codecademy/gamut-styles';
22
import { StyleProps } from '@codecademy/variance';
33
import styled from '@emotion/styled';
44
import { ComponentProps } from 'react';
55
import * as React from 'react';
66

7+
import { Anchor } from '../../Anchor';
78
import { Box } from '../../Box';
9+
import { Markdown } from '../../Markdown';
810
import { BaseInputProps } from '../types';
911
import { FormError } from './FormError';
1012
import { FormGroupDescription } from './FormGroupDescription';
@@ -20,6 +22,8 @@ export interface FormGroupProps
2022
error?: string;
2123
description?: string;
2224
labelSize?: 'small' | 'large';
25+
isFirstError?: boolean;
26+
errorType?: 'initial' | 'absolute';
2327
}
2428

2529
const formGroupSpacing = variant({
@@ -60,6 +64,12 @@ const FormGroupContainer: React.FC<
6064
return <StyledFormGroupContainer mb={mb} pb={pb} {...rest} />;
6165
};
6266

67+
const ErrorAnchor = styled(Anchor)(
68+
css({
69+
color: 'feedback-error',
70+
})
71+
);
72+
6373
export const FormGroup: React.FC<FormGroupProps> = ({
6474
children,
6575
className,
@@ -72,6 +82,8 @@ export const FormGroup: React.FC<FormGroupProps> = ({
7282
labelSize,
7383
required,
7484
isSoloField,
85+
isFirstError,
86+
errorType,
7587
...rest
7688
}) => {
7789
const labelComponent = label ? (
@@ -98,11 +110,36 @@ export const FormGroup: React.FC<FormGroupProps> = ({
98110
{labelComponent}
99111
{descriptionComponent}
100112
{children}
101-
{error && (
102-
<FormError aria-live="polite" role="alert">
103-
{error}
104-
</FormError>
105-
)}
113+
{/*
114+
* For screen readers to read new content, role="alert" and/or
115+
* aria-live wrapper elements must be present *before* content is
116+
* added. Thus, we need to render the FormError span always,
117+
* regardless of whether or not there is an error.
118+
*/}
119+
<FormError
120+
aria-live={isFirstError ? 'assertive' : 'off'}
121+
role={isFirstError ? 'alert' : 'status'}
122+
variant={errorType}
123+
>
124+
{error && (
125+
<Markdown
126+
inline
127+
overrides={{
128+
a: {
129+
allowedAttributes: ['href', 'target'],
130+
component: ErrorAnchor,
131+
processNode: (
132+
node: unknown,
133+
props: { onClick?: () => void }
134+
) => <ErrorAnchor {...props} />,
135+
},
136+
}}
137+
skipDefaultOverrides={{ a: true }}
138+
spacing="none"
139+
text={error}
140+
/>
141+
)}
142+
</FormError>
106143
</FormGroupContainer>
107144
);
108145
};

packages/gamut/src/GridForm/GridFormInputGroup/index.tsx

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import { css } from '@codecademy/gamut-styles';
2-
import styled from '@emotion/styled';
31
import * as React from 'react';
42
import { UseFormReturn } from 'react-hook-form';
53

6-
import { Anchor } from '../../Anchor';
7-
import { FormError, FormGroup, FormGroupLabel } from '../../Form';
4+
import { FormGroup, FormGroupLabel } from '../../Form';
85
import { HiddenText } from '../../HiddenText';
96
import { Column } from '../../Layout';
10-
import { Markdown } from '../../Markdown';
117
import {
128
GridFormField,
139
GridFormHiddenField,
@@ -23,11 +19,7 @@ import { GridFormSweetContainerInput } from './GridFormSweetContainerInput';
2319
import { GridFormTextArea } from './GridFormTextArea';
2420
import { GridFormTextInput } from './GridFormTextInput';
2521

26-
const ErrorAnchor = styled(Anchor)(
27-
css({
28-
color: 'feedback-error',
29-
})
30-
);
22+
3123

3224
export type GridFormInputGroupProps = {
3325
error?: string;
@@ -157,33 +149,14 @@ export const GridFormInputGroup: React.FC<GridFormInputGroupProps> = ({
157149

158150
return (
159151
<Column rowspan={field?.rowspan ?? 1} size={field?.size}>
160-
<FormGroup spacing={isTightCheckbox ? 'tight' : 'padded'}>
152+
<FormGroup
153+
error={errorMessage}
154+
errorType={isTightCheckbox ? 'initial' : 'absolute'}
155+
isFirstError={isFirstError}
156+
spacing={isTightCheckbox ? 'tight' : 'padded'}
157+
>
161158
{field.hideLabel ? <HiddenText>{label}</HiddenText> : label}
162159
{getInput()}
163-
{errorMessage && (
164-
<FormError
165-
aria-live={isFirstError ? 'assertive' : 'off'}
166-
role={isFirstError ? 'alert' : 'status'}
167-
variant={isTightCheckbox ? 'initial' : 'absolute'}
168-
>
169-
<Markdown
170-
inline
171-
overrides={{
172-
a: {
173-
allowedAttributes: ['href', 'target'],
174-
component: ErrorAnchor,
175-
processNode: (
176-
node: unknown,
177-
props: { onClick?: () => void }
178-
) => <ErrorAnchor {...props} />,
179-
},
180-
}}
181-
skipDefaultOverrides={{ a: true }}
182-
spacing="none"
183-
text={errorMessage}
184-
/>
185-
</FormError>
186-
)}
187160
</FormGroup>
188161
</Column>
189162
);

packages/gamut/src/GridForm/__tests__/GridForm.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ describe('GridForm', () => {
196196

197197
// there should only be a single "assertive" error from the form submission
198198
expect(view.getAllByRole('alert').length).toBe(1);
199-
expect(view.getAllByRole('status').length).toBe(1);
199+
expect(view.getAllByRole('status').length).toBe(2);
200200
});
201201

202202
describe('when "onSubmit" validation is selected', () => {

0 commit comments

Comments
 (0)