Skip to content

Revisions: Specify block level diff status via aria-label#77779

Open
himanshupathak95 wants to merge 12 commits into
WordPress:trunkfrom
himanshupathak95:fix/77530-block-level-diff-aria-labels
Open

Revisions: Specify block level diff status via aria-label#77779
himanshupathak95 wants to merge 12 commits into
WordPress:trunkfrom
himanshupathak95:fix/77530-block-level-diff-aria-labels

Conversation

@himanshupathak95

@himanshupathak95 himanshupathak95 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

What?

Part of #77530

Adds an aria-label to blocks in the revisions canvas that reflects their diff status (added, removed, or modified).

Why?

Screen reader users had no programmatic indication of a block's diff status when browsing the revisions canvas. Navigating to a changed block would only announce the block type (e.g. "Paragraph block") with no mention of what changed.

How?

Adds a useEffect inside withRevisionDiffClasses that writes aria-label directly to the block's DOM element after render, using useBlockElementRef to get a ref to the element.

A helper getDiffStatusLabel() builds the translatable label string based on the diff status and block type title:

  • Added Block: Paragraph
  • Removed Block: Image
  • Modified Block: Heading

Testing Instructions

  1. Create a post with several blocks (Paragraph, Heading, Image, etc.)
  2. Save, then edit some blocks - change text, remove a block, add a new one.
  3. Save again to create multiple revisions.
  4. Open the revisions panel from the editor sidebar.
  5. Use the slider to compare two revisions.
  6. Navigate through the blocks in the canvas.
  7. Verify the aria label depicts the diff status, e.g., "Modified Block: Paragraph", "Removed Block: Image".

Screenshots or screencast

Before

Screen.Recording.2026-04-28.at.19.26.39.mov

After

Screen.Recording.2026-04-28.at.16.54.14.mov

@github-actions github-actions Bot added the [Package] Editor /packages/editor label Apr 29, 2026
@himanshupathak95 himanshupathak95 force-pushed the fix/77530-block-level-diff-aria-labels branch 2 times, most recently from 8352414 to 50879fd Compare April 29, 2026 14:06
@himanshupathak95 himanshupathak95 marked this pull request as ready for review April 29, 2026 14:08
@github-actions

github-actions Bot commented Apr 29, 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: himanshupathak95 <abcd95@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>

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

@t-hamano t-hamano added [Type] Bug An existing feature does not function as intended [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). [Feature] History History, undo, redo, revisions, autosave. labels May 11, 2026
@github-project-automation github-project-automation Bot moved this to 🔎 Needs Review in WordPress 7.0 Editor Tasks May 11, 2026
@t-hamano t-hamano mentioned this pull request May 11, 2026
19 tasks
Comment on lines +126 to +147
const blockRef = useRef();
useBlockElementRef( block.clientId, blockRef );

useEffect( () => {
const el = blockRef.current;
if ( ! el || ! diffStatus ) {
el?.removeAttribute( 'aria-label' );
return;
}

const blockTitle = getBlockType( block.name )?.title;
if ( ! blockTitle ) {
return;
}

const diffLabel = getDiffStatusLabel( diffStatus, blockTitle );
if ( ! diffLabel ) {
return;
}

el.setAttribute( 'aria-label', diffLabel );
}, [ block.clientId, diffStatus, block.name ] );

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.

I feel this approach is risky because it modifies the DOM from the outside, which should be managed by other components themselves, leading to unpredictable behavior.

I haven't tested it, but is it possible to inject the diff label via wrapperProps?

diff --git a/packages/editor/src/components/post-revisions-preview/revisions-canvas.js b/packages/editor/src/components/post-revisions-preview/revisions-canvas.js
index 0e9552dff01..852a27803db 100644
--- a/packages/editor/src/components/post-revisions-preview/revisions-canvas.js
+++ b/packages/editor/src/components/post-revisions-preview/revisions-canvas.js
@@ -9,7 +9,7 @@ import clsx from 'clsx';
 import { Spinner } from '@wordpress/components';
 import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
 import { useSelect } from '@wordpress/data';
-import { useEffect, useRef } from '@wordpress/element';
+import { useEffect } from '@wordpress/element';
 import { addFilter } from '@wordpress/hooks';
 import { getBlockType } from '@wordpress/blocks';
 import { sprintf, __ } from '@wordpress/i18n';
@@ -26,9 +26,7 @@ import {
 } from './diff-format-types';
 import { useDiffMarkers } from './diff-markers';
 
-const { usePrivateStyleOverride, useBlockElementRef } = unlock(
-	blockEditorPrivateApis
-);
+const { usePrivateStyleOverride } = unlock( blockEditorPrivateApis );
 
 // SVG filter for removed blocks: grayscale + red tint
 const REVISION_REMOVED_FILTER_SVG = `
@@ -120,31 +118,14 @@ function getDiffStatusLabel( status, blockTitle ) {
  */
 function withRevisionDiffClasses( BlockListBlock ) {
 	return ( props ) => {
-		const { block, className } = props;
+		const { block, className, wrapperProps } = props;
 		const diffStatus = block?.__revisionDiffStatus?.status;
 
-		const blockRef = useRef();
-		useBlockElementRef( block.clientId, blockRef );
-
-		useEffect( () => {
-			const el = blockRef.current;
-			if ( ! el || ! diffStatus ) {
-				el?.removeAttribute( 'aria-label' );
-				return;
-			}
-
-			const blockTitle = getBlockType( block.name )?.title;
-			if ( ! blockTitle ) {
-				return;
-			}
-
-			const diffLabel = getDiffStatusLabel( diffStatus, blockTitle );
-			if ( ! diffLabel ) {
-				return;
-			}
-
-			el.setAttribute( 'aria-label', diffLabel );
-		}, [ block.clientId, diffStatus, block.name ] );
+		const blockTitle = getBlockType( block.name )?.title;
+		const diffLabel =
+			diffStatus && blockTitle
+				? getDiffStatusLabel( diffStatus, blockTitle )
+				: undefined;
 
 		const enhancedClassName = clsx( className, {
 			'is-revision-added': diffStatus === 'added',
@@ -152,7 +133,16 @@ function withRevisionDiffClasses( BlockListBlock ) {
 			'is-revision-modified': diffStatus === 'modified',
 		} );
 
-		return <BlockListBlock { ...props } className={ enhancedClassName } />;
+		return (
+			<BlockListBlock
+				{ ...props }
+				className={ enhancedClassName }
+				wrapperProps={ {
+					...wrapperProps,
+					...( diffLabel && { 'aria-label': diffLabel } ),
+				} }
+			/>
+		);
 	};
 }

@t-hamano t-hamano requested a review from ellatrix May 19, 2026 13:14
@himanshupathak95

himanshupathak95 commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

So, I was working on this to use the wrapperProps as suggested. I replaced the useBlockElementRef + useEffect + setAttribute with a wrapperProps containing 'aria-label'.

Here's what I found -


useBlockProps assembles the final props with 'aria-label': blockLabel as an explicit key after ...wrapperProps, so the spread is overridden. I think we need to change this to-

-		'aria-label': blockLabel,
+		'aria-label': wrapperProps[ 'aria-label' ] || blockLabel,

Also, I found out that paragraph block ignores blockProps['aria-label']

The Paragraph block sets aria-label explicitly, after spreading { ...blockProps }:

<RichText
identifier="content"
tagName="p"
{ ...blockProps }
value={ content }
onChange={ ( newContent ) =>
setAttributes( { content: newContent } )
}
onMerge={ mergeBlocks }
onReplace={ onReplace }
onRemove={ onRemove }
aria-label={
RichText.isEmpty( content )
? __(
'Empty block; start writing or type forward slash to choose a block'
)
: __( 'Block: Paragraph' )
}

Simplest fix I can think of is to destructure aria-label out of blockProps in paragraph block's edit, and prefer the injected value when it differs from the default. Somethig like this -

const { 'aria-label': injectedAriaLabel, ...restBlockProps } = blockProps;
const defaultLabel = __( 'Block: Paragraph' );

<RichText
    { ...restBlockProps }
    aria-label={
        injectedAriaLabel !== defaultLabel
            ? injectedAriaLabel
            : RichText.isEmpty( content )
            ? __( 'Empty block; start writing or type forward slash to choose a block' )
            : defaultLabel
    }
/>

Does this approach sound good?

@t-hamano

Copy link
Copy Markdown
Contributor

Here's what I found -


useBlockProps assembles the final props with 'aria-label': blockLabel as an explicit key after ...wrapperProps, so the spread is overridden. I think we need to change this to-

-		'aria-label': blockLabel,
+		'aria-label': wrapperProps[ 'aria-label' ] || blockLabel,

This seems like the right approach to me. cc @ellatrix

@himanshupathak95 himanshupathak95 force-pushed the fix/77530-block-level-diff-aria-labels branch from 50879fd to 3547840 Compare May 25, 2026 10:51
@github-actions github-actions Bot added [Package] Block library /packages/block-library [Package] Block editor /packages/block-editor labels May 25, 2026
@himanshupathak95

Copy link
Copy Markdown
Contributor Author

There was an e2e test failing earlier likely introduced by the paragraph/edit.js change.

I think the reason was that the test inserts a "Success Message" block variation. Active variations change blockTitle, so blockLabel becomes "Block: Success Message". The previous code compared injectedAriaLabel against the hardcoded "Block: Paragraph" and since "Block: Success Message" !== "Block: Paragraph", the condition mistakenly treated the variation label as a diff-injected label, changing the paragraph's aria-label away from "Block: Paragraph". The test, which selects by { name: 'Block: Paragraph' }, then couldn't find the element.

I reconstructed blockLabel from blockProps['data-title'] and compared against that instead.

@t-hamano

t-hamano commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

I believe the approach of this PR is correct, but I would also appreciate any feedback from @ellatrix.

const blockEditingMode = useBlockEditingMode();
const { 'aria-label': injectedAriaLabel, ...restBlockProps } = blockProps;
// translators: %s: block type title e.g. "Paragraph"
const blockLabel = sprintf( __( 'Block: %s' ), blockProps[ 'data-title' ] );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think it's a good idea to force blocks to change. Why are moving this here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can't we apply the label after spreading rich text props in the block props?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applying it after the spread was overriding the injected diff labels.

id: `block-${ clientId }${ htmlSuffix }`,
role: 'document',
'aria-label': blockLabel,
'aria-label': wrapperProps[ 'aria-label' ] || blockLabel,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should be careful not to introduce a new API here. If it wasn't possible before for extenders to set an aria label, then I'm not sure if we should add that. Perhaps we can check the store if revisions mode is on, or add some React context?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @ellatrix, thanks for the feedback!

What do you think about using a private React context (e.g., BlockAriaLabelOverrideContext exported via lock-unlock)? We could provide the diff label from revisions-canvas.js and consume it directly in useBlockProps.

This approach lets us completely revert the changes to the paragraph block and avoids passing anything through wrapperProps.

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

Labels

[Feature] History History, undo, redo, revisions, autosave. [Focus] Accessibility (a11y) Changes that impact accessibility and need corresponding review (e.g. markup changes). [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Package] Editor /packages/editor [Type] Bug An existing feature does not function as intended

Projects

Status: 🔎 Needs Review

Development

Successfully merging this pull request may close these issues.

3 participants