Skip to content

Commit

Permalink
a11y: Print script module live regions in page HTML (#65380)
Browse files Browse the repository at this point in the history
Fix issues where @wordpress/a11y script module `speak` may not be
detected and announced by accessibility software, in particular when
dynamically loading the module.

Accessibility software needs to detect the live regions in order to
announce them. The `@wordpress/a11y` script module was adding the live
regions to the DOM correctly when the module was loaded, but when loaded
dynamically (via `import( '@wordpress/a11y' ).then( /*…*/ )` ) the live
region changes would happen before the live region was detected by
accessibility software and the announcements would not happen.

The `@wordpress/a11y` script module is updated so that it does not
inject the live regions into the DOM but expects them to be present on
the page.

A hook is added to inject the necessary HTML for the live regions when
the @wordpress/a11y script module may be present (enqueued or in the
import map).

This has a few additional advantages:

- Accessibility software correctly detects the live regions.
- The DOM does not need to be modified immediately on page load.
- The (gzipped) size of the script module is reduced by 416B (~46%).


The `wp-a11y` script shares the same DOM elements. It includes
protection to prevent injecting duplicate elements.

---

Co-authored-by: sirreal <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: afercia <[email protected]>
  • Loading branch information
4 people committed Sep 17, 2024
1 parent be8f9b6 commit 05660fe
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 59 deletions.
49 changes: 42 additions & 7 deletions lib/experimental/script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,45 @@ function gutenberg_dequeue_module( $module_identifier ) {
wp_script_modules()->dequeue( $module_identifier );
}

/**
* Prints HTML for the a11y Script Module.
*
* a11y relies on some DOM elements to use as ARIA live regions.
* Ideally, these elements are part of the initial HTML of the page
* so that accessibility tools can find them and observe updates.
*/
function gutenberg_a11y_script_module_html() {
$a11y_module_available = false;

$get_marked_for_enqueue = new ReflectionMethod( 'WP_Script_Modules', 'get_marked_for_enqueue' );
$get_marked_for_enqueue->setAccessible( true );
$get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' );
$get_import_map->setAccessible( true );

foreach ( array_keys( $get_marked_for_enqueue->invoke( wp_script_modules() ) ) as $id ) {
if ( '@wordpress/a11y' === $id ) {
$a11y_module_available = true;
break;
}
}
if ( ! $a11y_module_available ) {
foreach ( array_keys( $get_import_map->invoke( wp_script_modules() )['imports'] ) as $id ) {
if ( '@wordpress/a11y' === $id ) {
$a11y_module_available = true;
break;
}
}
}
if ( ! $a11y_module_available ) {
return;
}
echo '<div style="position:absolute;margin:-1px;padding:0;height:1px;width:1px;overflow:hidden;clip-path:inset(50%);border:0;word-wrap:normal !important;">'
. '<p id="a11y-speak-intro-text" class="a11y-speak-intro-text" hidden>' . esc_html__( 'Notifications', 'default' ) . '</p>'
. '<div id="a11y-speak-assertive" class="a11y-speak-region" aria-live="assertive" aria-relevant="additions text" aria-atomic="true"></div>'
. '<div id="a11y-speak-polite" class="a11y-speak-region" aria-live="polite" aria-relevant="additions text" aria-atomic="true"></div>'
. '</div>';
}

/**
* Registers Gutenberg Script Modules.
*
Expand All @@ -218,12 +257,8 @@ function gutenberg_register_script_modules() {
array(),
$default_version
);
add_filter(
'script_module_data_@wordpress/a11y',
function ( $data ) {
$data['i18n'] = array( 'Notifications' => __( 'Notifications', 'default' ) );
return $data;
}
);

add_action( 'wp_footer', 'gutenberg_a11y_script_module_html' );
add_action( 'admin_footer', 'gutenberg_a11y_script_module_html' );
}
add_action( 'init', 'gutenberg_register_script_modules' );
25 changes: 22 additions & 3 deletions packages/a11y/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,37 @@
* WordPress dependencies
*/
import domReady from '@wordpress/dom-ready';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { makeSetupFunction } from './shared/index';
import addContainer from './script/add-container';
import addIntroText from './script/add-intro-text';

export { speak } from './shared/index';

/**
* Create the live regions.
*/
export const setup = makeSetupFunction( __( 'Notifications' ) );
export function setup() {
const introText = document.getElementById( 'a11y-speak-intro-text' );
const containerAssertive = document.getElementById(
'a11y-speak-assertive'
);
const containerPolite = document.getElementById( 'a11y-speak-polite' );

if ( introText === null ) {
addIntroText();
}

if ( containerAssertive === null ) {
addContainer( 'assertive' );
}

if ( containerPolite === null ) {
addContainer( 'polite' );
}
}

/**
* Run setup on domReady.
Expand Down
22 changes: 4 additions & 18 deletions packages/a11y/src/module/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,11 @@
/**
* Internal dependencies
*/
import { makeSetupFunction } from '../shared/index';
export { speak } from '../shared/index';

// Without an i18n Script Module, "Notifications" (the only localized text used in this module)
// will be translated on the server and provided as script-module data.
let notificationsText = 'Notifications';
try {
const textContent = document.getElementById(
'wp-script-module-data-@wordpress/a11y'
)?.textContent;
if ( textContent ) {
const parsed = JSON.parse( textContent );
notificationsText = parsed?.i18n?.Notifications ?? notificationsText;
}
} catch {}

/**
* Create the live regions.
* This no-op function is exported to provide compatibility with the `wp-a11y` Script.
*
* Filters should inject the relevant HTML on page load instead of requiring setup.
*/
export const setup = makeSetupFunction( notificationsText );

setup();
export const setup = () => {};
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';

/**
* Build the explanatory text to be placed before the aria live regions.
*
* This text is initially hidden from assistive technologies by using a `hidden`
* HTML attribute which is then removed once a message fills the aria-live regions.
*
* @param {string} introTextContent The translated intro text content.
* @return {HTMLParagraphElement} The explanatory text HTML element.
*/
export default function addIntroText( introTextContent: string ) {
export default function addIntroText() {
const introText = document.createElement( 'p' );

introText.id = 'a11y-speak-intro-text';
introText.className = 'a11y-speak-intro-text';
introText.textContent = introTextContent;
introText.textContent = __( 'Notifications' );

introText.setAttribute(
'style',
Expand Down
28 changes: 0 additions & 28 deletions packages/a11y/src/shared/index.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,9 @@
/**
* Internal dependencies
*/
import addContainer from './add-container';
import addIntroText from './add-intro-text';
import clear from './clear';
import filterMessage from './filter-message';

/**
* Create the live regions.
* @param {string} introTextContent The intro text content.
*/
export function makeSetupFunction( introTextContent ) {
return function setup() {
const introText = document.getElementById( 'a11y-speak-intro-text' );
const containerAssertive = document.getElementById(
'a11y-speak-assertive'
);
const containerPolite = document.getElementById( 'a11y-speak-polite' );

if ( introText === null ) {
addIntroText( introTextContent );
}

if ( containerAssertive === null ) {
addContainer( 'assertive' );
}

if ( containerPolite === null ) {
addContainer( 'polite' );
}
};
}

/**
* Allows you to easily announce dynamic interface updates to screen readers using ARIA live regions.
* This module is inspired by the `speak` function in `wp-a11y.js`.
Expand Down

0 comments on commit 05660fe

Please sign in to comment.