Skip to content

Editor: Describe inline revision diffs with aria-describedby - #80207

Closed
thisismyurl wants to merge 1 commit into
WordPress:trunkfrom
thisismyurl:a11y-visual-history
Closed

thisismyurl wants to merge 1 commit into
WordPress:trunkfrom
thisismyurl:a11y-visual-history

Conversation

@thisismyurl

Copy link
Copy Markdown

What?

Part of #77530.
Core tracking ticket: https://core.trac.wordpress.org/ticket/65122

Gives the inline diff formats in Visual History (del, ins, and the format-change spans) a reliable accessible description: each one now carries aria-describedby pointing at a visually hidden description element rendered into the canvas document.

Why?

From @joedolson's audit in #77530:

Inline edits use the title attribute to communicate to screen readers. This works, but will have somewhat inconsistent support, since screen readers may ignore it in low-verbosity modes. Using something like aria-describedby pointing to a container would be more reliable.

The title attribute maps to an ARIA description in the accessibility tree, but that mapping is exactly what low-verbosity modes drop. An explicit aria-describedby reference wins the description computation and is announced consistently.

How?

  • diff-format-types.js registers aria-describedby as a format attribute alongside title, and exports a new useDiffDescriptionsRef() hook. The hook appends a hidden container of description elements (Removed, Added, Format added, Format removed, Format changed) to the document that hosts the ref element. That matters because the canvas is an iframe, and aria-describedby only resolves within the same document.
  • block-diff.js adds the matching ariaDescribedBy attribute at the three applyFormat() call sites.
  • revisions-canvas.js merges the new ref with the existing diff-markers ref on the canvas content element.

One trade-off worth flagging: the format-change spans previously carried detailed counts in title ("2 formats added, 1 format removed"). Those stay in title for the mouse tooltip, but the aria-describedby description is the generic per-type text ("Format added"), since the referenced elements are static. If you'd rather screen reader users get the detailed counts, the descriptions container would need to be built per-instance from the diff output. I kept this version minimal, but happy to build that instead if it's the right call.

I considered injecting the descriptions through the existing usePrivateStyleOverride assets channel (how the removed-blocks SVG filter gets into the iframe), but that channel renders inside an <svg> element, which is no place for HTML description elements. So the injection is ref-based instead, next to the existing diff-markers pattern.

Testing Instructions

  1. Create a post with a paragraph, save, edit the paragraph text (change a few words, bold one of them), and save again.
  2. Open the post's Visual History and enable the diff view.
  3. Inspect a del/ins/format-change span in the canvas iframe: each should have aria-describedby pointing at an element with the corresponding ID (revision-diff-description-removed, -added, -format-*), and that hidden container should exist in the iframe's body.
  4. Unit tests: npm run test:unit packages/editor/src/components/post-revisions-preview

To be upfront about the verification boundary: the unit tests prove the DOM structure (attributes present, correct IDs, hidden container mounted and cleaned up), but I haven't been able to run this against a live screen reader on this machine, so I'd appreciate an AT confirmation during review.

Testing Instructions for Keyboard

  1. With a screen reader running (NVDA/VoiceOver), navigate into the revision content and read through a changed paragraph.
  2. Removed text should be described as "Removed", added text as "Added", and format-only changes as "Format added/removed/changed", including in verbosity configurations where title-based descriptions are not announced.

Props @thisismyurl.

Use of AI Tools

(full disclosure: AI helped me identify the issue and verify my work)

The inline diff formats in the Visual History canvas communicated their
meaning to assistive technology only through the title attribute, which
screen readers may ignore in low-verbosity modes. Each format now also
references a visually hidden description element injected into the
canvas document, which is announced reliably. The title attribute stays
for the mouse-hover tooltip and the detailed format-change counts.

Part of WordPress#77530.
See https://core.trac.wordpress.org/ticket/65122.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 13, 2026 16:53
@github-actions

Copy link
Copy Markdown

Warning: Type of PR label mismatch

To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.

  • Required label: Any label starting with [Type].
  • Labels found: .

Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task.

@github-actions

github-actions Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: thisismyurl <thisismyurl@git.wordpress.org>
Co-authored-by: joedolson <joedolson@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions Bot added the [Package] Editor /packages/editor label Jul 13, 2026
@github-actions github-actions Bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Jul 13, 2026
@github-actions

Copy link
Copy Markdown

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @thisismyurl! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the accessibility of inline revision diffs in the Visual History canvas by adding stable aria-describedby descriptions for del, ins, and format-change spans (so AT doesn’t rely on title announcements).

Changes:

  • Register aria-describedby as a rich-text format attribute and add a useDiffDescriptionsRef() hook to inject description elements into the canvas document.
  • Apply the new ariaDescribedBy attribute at inline diff formatting call sites and merge the new ref with the existing diff-markers ref.
  • Add/update unit tests to validate the rendered attributes and injected description elements.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/editor/src/components/post-revisions-preview/diff-format-types.js Registers aria-describedby and injects description elements into the canvas document via a ref hook.
packages/editor/src/components/post-revisions-preview/block-diff.js Applies ariaDescribedBy IDs when marking added/removed/format-changed inline diffs.
packages/editor/src/components/post-revisions-preview/revisions-canvas.js Merges the diff-markers ref with the new descriptions ref on the canvas content element.
packages/editor/src/components/post-revisions-preview/test/block-diff.js Updates expected inline diff HTML strings to include aria-describedby.
packages/editor/src/components/post-revisions-preview/test/diff-format-types.js Adds tests for mounting/unmounting the injected description elements.

Comment on lines +84 to +88
const container = ownerDocument.createElement( 'div' );
// Hidden elements are still used for the accessible description
// computation when referenced by `aria-describedby`.
container.hidden = true;
for ( const { descriptionId, title } of DIFF_FORMAT_TYPES ) {
Comment on lines +31 to +35
for ( const [ id, text ] of Object.entries( EXPECTED_DESCRIPTIONS ) ) {
const description = screen.getByText( text );
expect( description ).toHaveAttribute( 'id', id );
expect( description ).not.toBeVisible();
}
@joedolson

Copy link
Copy Markdown
Contributor

Sorry that I didn't see this one; I reviewed #80440, which is very similar.

@thisismyurl

Copy link
Copy Markdown
Author

Two failing checks — both are non-code issues:

type-related-labels — The PR is missing a [Type] label. I don't have permissions to add labels, but [Type] Enhancement is the right choice (accessibility improvement to an existing feature). A reviewer can add it.

Run performance tests — Failed at the wp-env start step with AggregateError [ETIMEDOUT] — a Docker image pull network timeout on the CI runner, not a code regression. Rerunning the workflow should clear it (also requires maintainer permissions).

No code changes needed for either.

@thisismyurl

Copy link
Copy Markdown
Author

Confirmed — pulling in trunk turns up a straight duplicate of #80440 (same IDs wired into block-diff.js and revisions-canvas.jsx, same mechanism). Closing this one rather than rebasing a redundant reimplementation on top of it. Thanks for catching it, @joedolson.

@thisismyurl
thisismyurl deleted the a11y-visual-history branch September 22, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Package] Editor /packages/editor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants