Skip to content

Commit

Permalink
A11y: Add Script Module (#65101)
Browse files Browse the repository at this point in the history
Add a `@wordpress/a11y` WordPress Script Module. The script module has the same public API as the `wp-a11y` script.

- The `domReady` package should not be necessary. Since modules are deferred, setup can be called directly when the module is evaluated.
- There is no i18n script module at this time. The necessary string "Notifications" is translated on the server and passed to the package via script module data.
- Most of the changes here are moving some functions around so that the script and the module form of this package rely on the same underlying implementations.

---

Co-authored-by: sirreal <[email protected]>
Co-authored-by: gziolo <[email protected]>
  • Loading branch information
3 people authored Sep 12, 2024
1 parent 9bdfebf commit f88e629
Show file tree
Hide file tree
Showing 16 changed files with 151 additions and 87 deletions.
27 changes: 27 additions & 0 deletions lib/experimental/script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,30 @@ function gutenberg_dequeue_module( $module_identifier ) {
_deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_dequeue_script_module' );
wp_script_modules()->dequeue( $module_identifier );
}

/**
* Registers Gutenberg Script Modules.
*
* @since 19.3
*/
function gutenberg_register_script_modules() {
// When in production, use the plugin's version as the default asset version;
// else (for development or test) default to use the current time.
$default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time();

wp_deregister_script_module( '@wordpress/a11y' );
wp_register_script_module(
'@wordpress/a11y',
gutenberg_url( 'build-module/a11y/index.min.js' ),
array(),
$default_version
);
add_filter(
'script_module_data_@wordpress/a11y',
function ( $data ) {
$data['i18n'] = array( 'Notifications' => __( 'Notifications', 'default' ) );
return $data;
}
);
}
add_action( 'init', 'gutenberg_register_script_modules' );
2 changes: 1 addition & 1 deletion packages/a11y/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ speak( 'The message you want to send to the ARIA live region', 'assertive' );
_Parameters_

- _message_ `string`: The message to be announced by assistive technologies.
- _ariaLive_ `[string]`: The politeness level for aria-live; default: 'polite'.
- _ariaLive_ `['polite'|'assertive']`: The politeness level for aria-live; default: 'polite'.

<!-- END TOKEN(Autogenerated API docs) -->

Expand Down
1 change: 1 addition & 0 deletions packages/a11y/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"module": "build-module/index.js",
"react-native": "src/index",
"types": "build-types",
"wpScriptModuleExports": "./build-module/module/index.js",
"dependencies": {
"@babel/runtime": "^7.16.0",
"@wordpress/dom-ready": "file:../dom-ready",
Expand Down
75 changes: 4 additions & 71 deletions packages/a11y/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,87 +2,20 @@
* WordPress dependencies
*/
import domReady from '@wordpress/dom-ready';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import addIntroText from './add-intro-text';
import addContainer from './add-container';
import clear from './clear';
import filterMessage from './filter-message';
import { makeSetupFunction } from './shared/index';
export { speak } from './shared/index';

/**
* Create the live regions.
*/
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' );
}
}
export const setup = makeSetupFunction( __( 'Notifications' ) );

/**
* Run setup on domReady.
*/
domReady( setup );

/**
* 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`.
*
* @param {string} message The message to be announced by assistive technologies.
* @param {string} [ariaLive] The politeness level for aria-live; default: 'polite'.
*
* @example
* ```js
* import { speak } from '@wordpress/a11y';
*
* // For polite messages that shouldn't interrupt what screen readers are currently announcing.
* speak( 'The message you want to send to the ARIA live region' );
*
* // For assertive messages that should interrupt what screen readers are currently announcing.
* speak( 'The message you want to send to the ARIA live region', 'assertive' );
* ```
*/
export function speak( message, ariaLive ) {
/*
* Clear previous messages to allow repeated strings being read out and hide
* the explanatory text from assistive technologies.
*/
clear();

message = filterMessage( message );

const introText = document.getElementById( 'a11y-speak-intro-text' );
const containerAssertive = document.getElementById(
'a11y-speak-assertive'
);
const containerPolite = document.getElementById( 'a11y-speak-polite' );

if ( containerAssertive && ariaLive === 'assertive' ) {
containerAssertive.textContent = message;
} else if ( containerPolite ) {
containerPolite.textContent = message;
}

/*
* Make the explanatory text available to assistive technologies by removing
* the 'hidden' HTML attribute.
*/
if ( introText ) {
introText.removeAttribute( 'hidden' );
}
}
2 changes: 1 addition & 1 deletion packages/a11y/src/index.native.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import filterMessage from './filter-message';
import filterMessage from './shared/filter-message';

/**
* Update the ARIA live notification area text node.
Expand Down
25 changes: 25 additions & 0 deletions packages/a11y/src/module/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* 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.
*/
export const setup = makeSetupFunction( notificationsText );

setup();
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
/**
* 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() {
export default function addIntroText( introTextContent: string ) {
const introText = document.createElement( 'p' );

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

introText.setAttribute(
'style',
Expand Down
File renamed without changes.
File renamed without changes.
81 changes: 81 additions & 0 deletions packages/a11y/src/shared/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* 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`.
*
* @param {string} message The message to be announced by assistive technologies.
* @param {'polite'|'assertive'} [ariaLive] The politeness level for aria-live; default: 'polite'.
*
* @example
* ```js
* import { speak } from '@wordpress/a11y';
*
* // For polite messages that shouldn't interrupt what screen readers are currently announcing.
* speak( 'The message you want to send to the ARIA live region' );
*
* // For assertive messages that should interrupt what screen readers are currently announcing.
* speak( 'The message you want to send to the ARIA live region', 'assertive' );
* ```
*/
export function speak( message, ariaLive ) {
/*
* Clear previous messages to allow repeated strings being read out and hide
* the explanatory text from assistive technologies.
*/
clear();

message = filterMessage( message );

const introText = document.getElementById( 'a11y-speak-intro-text' );
const containerAssertive = document.getElementById(
'a11y-speak-assertive'
);
const containerPolite = document.getElementById( 'a11y-speak-polite' );

if ( containerAssertive && ariaLive === 'assertive' ) {
containerAssertive.textContent = message;
} else if ( containerPolite ) {
containerPolite.textContent = message;
}

/*
* Make the explanatory text available to assistive technologies by removing
* the 'hidden' HTML attribute.
*/
if ( introText ) {
introText.removeAttribute( 'hidden' );
}
}
File renamed without changes.
8 changes: 4 additions & 4 deletions packages/a11y/src/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ import domReady from '@wordpress/dom-ready';
* Internal dependencies
*/
import { setup, speak } from '../';
import clear from '../clear';
import filterMessage from '../filter-message';
import clear from '../shared/clear';
import filterMessage from '../shared/filter-message';

jest.mock( '../clear', () => {
jest.mock( '../shared/clear', () => {
return jest.fn();
} );
jest.mock( '@wordpress/dom-ready', () => {
return jest.fn( ( callback ) => {
callback();
} );
} );
jest.mock( '../filter-message', () => {
jest.mock( '../shared/filter-message', () => {
return jest.fn( ( message ) => {
return message;
} );
Expand Down
7 changes: 4 additions & 3 deletions packages/dependency-extraction-webpack-plugin/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ function defaultRequestToExternalModule( request ) {
return `module ${ request }`;
}

if ( request === '@wordpress/interactivity-router' ) {
// Assumes this is usually going to be used as a dynamic import.
return `import ${ request }`;
switch ( request ) {
case '@wordpress/interactivity-router':
case '@wordpress/a11y':
return `import ${ request }`;
}

const isWordPressScript = Boolean( defaultRequestToExternal( request ) );
Expand Down

0 comments on commit f88e629

Please sign in to comment.