Skip to content

Omit redundant role="heading" when rendering native h1-h6 elements#2845

Open
KAMRONBEK wants to merge 1 commit into
necolas:masterfrom
KAMRONBEK:fix-redundant-heading-role
Open

Omit redundant role="heading" when rendering native h1-h6 elements#2845
KAMRONBEK wants to merge 1 commit into
necolas:masterfrom
KAMRONBEK:fix-redundant-heading-role

Conversation

@KAMRONBEK

Copy link
Copy Markdown

Problem

<Text accessibilityRole="header"> (or role="heading") renders <h1 role="heading">. The explicit role is redundant on a native heading element per the W3C ARIA-in-HTML spec, and it fails axe-core audits ("Page must contain a level-one heading" reports h1[role="heading"] as insufficient).

Root cause

createElement already maps the heading role to a semantic h1h6 element via AccessibilityUtil.propsToAccessibilityComponent (using aria-level/accessibilityLevel, defaulting to h1), but createDOMProps then unconditionally emits domProps.role from propsToAriaRole, duplicating the semantics on the DOM node.

Fix

In modules/createDOMProps/index.js, skip emitting the role attribute only when it would be redundant: role === 'heading' and the resolved elementType is already h1h6. All other roles, and role="heading" on non-heading elements (e.g. a <div role="heading" aria-level="2">), are emitted exactly as before. (Other roles that map to native elements, e.g. button, have the same pattern — this PR is scoped to heading, the case axe flags and #2780 reports; happy to extend if preferred.)

Tests

Added cases to modules/createDOMProps/__tests__/index-test.js:

  • role: 'heading' on each of h1h6 emits no role attribute
  • legacy accessibilityRole: 'header' on h1 emits no role attribute
  • role: 'heading' + aria-level on a div still emits role="heading" (and keeps aria-level)
  • accessibilityRole: 'header' on a div still emits role="heading"

The two heading-element tests fail without the fix. Full jest dom + node suites, prettier --check, eslint, and flow all pass (no existing snapshots reference the heading role).

Fixes #2780

When role='heading' (or the deprecated accessibilityRole='header') is
used, createElement already maps the element to h1-h6 via
propsToAccessibilityComponent, but createDOMProps still emitted
role='heading' on the DOM node. <h1 role="heading"> is a redundant
role per ARIA in HTML and fails axe audits.

Skip the role attribute when the resolved element type is already a
native heading element. Non-heading elements with role='heading' are
unaffected.

Fix necolas#2780
@codesandbox-ci

Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 975af4d:

Sandbox Source
react-native-web-examples Configuration

@KAMRONBEK

Copy link
Copy Markdown
Author

@necolas TL;DR: role="heading" (or legacy accessibilityRole="header") already renders a semantic h1h6 via propsToAccessibilityComponent, but createDOMProps then also emits role="heading" on it — redundant per ARIA-in-HTML and flagged by axe-core. This skips the role attribute only when the resolved element is already h1h6; role="heading" on non-heading elements (e.g. div + aria-level) is emitted exactly as before. Scoped to heading per #2780 — happy to extend to the other element-mapped roles (button, ul/li, …) if you'd prefer that in one pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A11y Text accessible role "heading" render redundant role

1 participant