From 76dfcbaf720d47cd8e30b8df0a88ea53feb3aa49 Mon Sep 17 00:00:00 2001 From: John Rood Date: Tue, 29 Jul 2025 17:10:46 -0500 Subject: [PATCH] Render alert elements prior to new content --- .../src/ConnectedForm/ConnectedFormGroup.tsx | 33 +++++++------ .../gamut/src/Form/elements/FormGroup.tsx | 49 ++++++++++++++++--- .../src/GridForm/GridFormInputGroup/index.tsx | 43 +++------------- .../src/GridForm/__tests__/GridForm.test.tsx | 2 +- 4 files changed, 70 insertions(+), 57 deletions(-) diff --git a/packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx b/packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx index 90944d44aed..6e4a2756e8a 100644 --- a/packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx +++ b/packages/gamut/src/ConnectedForm/ConnectedFormGroup.tsx @@ -55,7 +55,6 @@ export function ConnectedFormGroup({ id, label, name, - labelSize, spacing = 'fit', isSoloField, infotip, @@ -82,7 +81,6 @@ export function ConnectedFormGroup({ infotip={infotip} isSoloField={isSoloField} required={!!validation?.required} - size={labelSize} > {label} @@ -103,31 +101,36 @@ export function ConnectedFormGroup({ name={name} /> {children} - {showError && ( - + {/* + * For screen readers to read new content, role="alert" and/or + * aria-live wrapper elements must be present *before* content is + * added. Thus, we need to render the FormError span always, + * regardless of whether or not there is an error. + */} + + {showError && ( void } - ) => , + processNode: (_: unknown, props: { onClick?: () => void }) => ( + + ), }, }} skipDefaultOverrides={{ a: true }} spacing="none" text={textError} /> - - )} + )} + ); } diff --git a/packages/gamut/src/Form/elements/FormGroup.tsx b/packages/gamut/src/Form/elements/FormGroup.tsx index e7e302b77c5..df219117d85 100644 --- a/packages/gamut/src/Form/elements/FormGroup.tsx +++ b/packages/gamut/src/Form/elements/FormGroup.tsx @@ -1,10 +1,12 @@ -import { variant } from '@codecademy/gamut-styles'; +import { css, variant } from '@codecademy/gamut-styles'; import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; import { ComponentProps } from 'react'; import * as React from 'react'; +import { Anchor } from '../../Anchor'; import { Box } from '../../Box'; +import { Markdown } from '../../Markdown'; import { BaseInputProps } from '../types'; import { FormError } from './FormError'; import { FormGroupDescription } from './FormGroupDescription'; @@ -20,6 +22,8 @@ export interface FormGroupProps error?: string; description?: string; labelSize?: 'small' | 'large'; + isFirstError?: boolean; + errorType?: 'initial' | 'absolute'; } const formGroupSpacing = variant({ @@ -60,6 +64,12 @@ const FormGroupContainer: React.FC< return ; }; +const ErrorAnchor = styled(Anchor)( + css({ + color: 'feedback-error', + }) +); + export const FormGroup: React.FC = ({ children, className, @@ -72,6 +82,8 @@ export const FormGroup: React.FC = ({ labelSize, required, isSoloField, + isFirstError, + errorType, ...rest }) => { const labelComponent = label ? ( @@ -98,11 +110,36 @@ export const FormGroup: React.FC = ({ {labelComponent} {descriptionComponent} {children} - {error && ( - - {error} - - )} + {/* + * For screen readers to read new content, role="alert" and/or + * aria-live wrapper elements must be present *before* content is + * added. Thus, we need to render the FormError span always, + * regardless of whether or not there is an error. + */} + + {error && ( + void } + ) => , + }, + }} + skipDefaultOverrides={{ a: true }} + spacing="none" + text={error} + /> + )} + ); }; diff --git a/packages/gamut/src/GridForm/GridFormInputGroup/index.tsx b/packages/gamut/src/GridForm/GridFormInputGroup/index.tsx index 7fa3b5a9f00..755f8b2e372 100644 --- a/packages/gamut/src/GridForm/GridFormInputGroup/index.tsx +++ b/packages/gamut/src/GridForm/GridFormInputGroup/index.tsx @@ -1,13 +1,9 @@ -import { css } from '@codecademy/gamut-styles'; -import styled from '@emotion/styled'; import * as React from 'react'; import { UseFormReturn } from 'react-hook-form'; -import { Anchor } from '../../Anchor'; -import { FormError, FormGroup, FormGroupLabel } from '../../Form'; +import { FormGroup, FormGroupLabel } from '../../Form'; import { HiddenText } from '../../HiddenText'; import { Column } from '../../Layout'; -import { Markdown } from '../../Markdown'; import { GridFormField, GridFormHiddenField, @@ -23,11 +19,7 @@ import { GridFormSweetContainerInput } from './GridFormSweetContainerInput'; import { GridFormTextArea } from './GridFormTextArea'; import { GridFormTextInput } from './GridFormTextInput'; -const ErrorAnchor = styled(Anchor)( - css({ - color: 'feedback-error', - }) -); + export type GridFormInputGroupProps = { error?: string; @@ -157,33 +149,14 @@ export const GridFormInputGroup: React.FC = ({ return ( - + {field.hideLabel ? {label} : label} {getInput()} - {errorMessage && ( - - void } - ) => , - }, - }} - skipDefaultOverrides={{ a: true }} - spacing="none" - text={errorMessage} - /> - - )} ); diff --git a/packages/gamut/src/GridForm/__tests__/GridForm.test.tsx b/packages/gamut/src/GridForm/__tests__/GridForm.test.tsx index 1c48254a233..60d25f45188 100644 --- a/packages/gamut/src/GridForm/__tests__/GridForm.test.tsx +++ b/packages/gamut/src/GridForm/__tests__/GridForm.test.tsx @@ -196,7 +196,7 @@ describe('GridForm', () => { // there should only be a single "assertive" error from the form submission expect(view.getAllByRole('alert').length).toBe(1); - expect(view.getAllByRole('status').length).toBe(1); + expect(view.getAllByRole('status').length).toBe(2); }); describe('when "onSubmit" validation is selected', () => {