-
Notifications
You must be signed in to change notification settings - Fork 17.2k
fix(a11y): WCAG 3.3.1 — add error identification with role=alert and aria-describedby #39245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b3b0593
f5aa012
7938a0d
c822114
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ | |
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| import { FC, ReactNode } from 'react'; | ||
| import { FC, ReactNode, useId } from 'react'; | ||
| import { t } from '@apache-superset/core/translation'; | ||
| import { css, useTheme, SupersetTheme } from '@apache-superset/core/theme'; | ||
| import { FormLabel, InfoTooltip, Tooltip } from '@superset-ui/core/components'; | ||
|
|
@@ -37,6 +37,10 @@ export type ControlHeaderProps = { | |
| tooltipOnClick?: () => void; | ||
| warning?: string; | ||
| danger?: string; | ||
| // WCAG 3.3.1: error container ID shared with the input's aria-describedby. | ||
| // Passed in by wrappers like NumberControl/TextControl so there is exactly | ||
| // one live-region and one DOM id per control (no duplicate ids/alerts). | ||
| errorId?: string; | ||
| // Allow extra props from control spread patterns (e.g. {...this.props}) | ||
| [key: string]: unknown; | ||
| }; | ||
|
|
@@ -70,8 +74,11 @@ const ControlHeader: FC<ControlHeaderProps> = ({ | |
| tooltipOnClick = () => {}, | ||
| warning, | ||
| danger, | ||
| errorId: providedErrorId, | ||
| }) => { | ||
| const theme = useTheme(); | ||
| const uniqueId = useId(); | ||
| const errorId = providedErrorId ?? `${name || uniqueId}-error`; | ||
|
|
||
| if (!label) { | ||
| return null; | ||
|
|
@@ -178,8 +185,28 @@ const ControlHeader: FC<ControlHeaderProps> = ({ | |
| placement="top" | ||
| title={validationErrors?.join(' ')} | ||
| > | ||
| <Icons.ExclamationCircleOutlined iconColor={theme.colorError} /> | ||
| </Tooltip>{' '} | ||
| <Icons.ExclamationCircleOutlined | ||
| iconColor={theme.colorError} | ||
| aria-hidden="true" | ||
| /> | ||
| </Tooltip> | ||
| <span | ||
| id={errorId} | ||
| role="alert" | ||
| css={css` | ||
| position: absolute; | ||
| width: 1px; | ||
| height: 1px; | ||
| padding: 0; | ||
| margin: -1px; | ||
| overflow: hidden; | ||
| clip: rect(0, 0, 0, 0); | ||
| white-space: nowrap; | ||
| border: 0; | ||
| `} | ||
| > | ||
| {validationErrors?.join('. ')} | ||
| </span>{' '} | ||
|
Comment on lines
+193
to
+209
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate HTML IDs in Error Handling
The added screen reader span improves accessibility by announcing validation errors, but the id attribute creates duplicate HTML ids when used with controls like NumberControl that already define an error span with the same id. This violates HTML uniqueness rules and could confuse assistive technologies. Removing the id maintains the alert functionality while fixing the conflict. Code suggestionCheck the AI-generated fix before applying Code Review Run #f7d2d1 Should Bito avoid suggestions like this for future reviews? (Manage Rules)
|
||
| </span> | ||
| )} | ||
| {renderOptionalIcons()} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,10 +18,18 @@ | |
| */ | ||
| import { Component, ChangeEvent } from 'react'; | ||
| import { legacyValidateNumber, legacyValidateInteger } from '@superset-ui/core'; | ||
| import { css, SupersetTheme } from '@apache-superset/core/theme'; | ||
| import { debounce } from 'lodash'; | ||
| import ControlHeader from 'src/explore/components/ControlHeader'; | ||
| import { Constants, Input } from '@superset-ui/core/components'; | ||
|
|
||
| const fieldErrorStyles = (theme: SupersetTheme) => css` | ||
| color: ${theme.colorError}; | ||
| font-size: ${theme.fontSizeSM}px; | ||
| display: block; | ||
| margin-top: ${theme.sizeUnit}px; | ||
| `; | ||
|
|
||
| type InputValueType = string | number; | ||
|
|
||
| export interface TextControlProps<T extends InputValueType = InputValueType> { | ||
|
|
@@ -104,9 +112,15 @@ export default class TextControl< | |
| this.initialValue = this.props.value; | ||
| value = safeStringify(this.props.value); | ||
| } | ||
| const hasErrors = | ||
| this.props.validationErrors && this.props.validationErrors.length > 0; | ||
| const inputId = this.props.controlId || this.props.name; | ||
| // WCAG 3.3.1: share a single error container id with ControlHeader so | ||
| // the input's aria-describedby resolves to one live-region, not two. | ||
| const errorId = hasErrors && inputId ? `${inputId}-error` : undefined; | ||
| return ( | ||
| <div> | ||
| <ControlHeader {...this.props} /> | ||
| <ControlHeader {...this.props} errorId={errorId} /> | ||
|
Comment on lines
+117
to
+123
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate error display
The ControlHeader component displays validation errors when the validationErrors prop is passed, but the new inline span also shows the same error text, resulting in duplicate error messages for users. To resolve this and ensure proper accessibility, omit validationErrors from ControlHeader props and add id and role attributes to the inline error span so it serves as the single error container referenced by aria-describedby. Code suggestionCheck the AI-generated fix before applying Code Review Run #38f541 Should Bito avoid suggestions like this for future reviews? (Manage Rules)
|
||
| <Input | ||
| type="text" | ||
| data-test="inline-name" | ||
|
|
@@ -116,7 +130,15 @@ export default class TextControl< | |
| value={value} | ||
| disabled={this.props.disabled} | ||
| aria-label={this.props.label} | ||
| aria-invalid={hasErrors || undefined} | ||
| aria-describedby={errorId} | ||
| id={inputId} | ||
| /> | ||
| {hasErrors && ( | ||
| <span css={fieldErrorStyles}> | ||
| {this.props.validationErrors!.join('. ')} | ||
| </span> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: The accessibility attributes are currently injected only into the first direct child, but many call sites pass wrappers like
FormItemas that child, so the real input never receivesaria-invalid/aria-describedby. This causes error association to fail at runtime for wrapped fields. Apply the attributes to the wrapped control element when the first child is a wrapper with a single valid child. [logic error]Severity Level: Critical 🚨
Steps of Reproduction ✅
Prompt for AI Agent 🤖