diff --git a/lib/interactivity-api.php b/lib/interactivity-api.php index 90535f1ebaa42..c00d68bc70e8e 100644 --- a/lib/interactivity-api.php +++ b/lib/interactivity-api.php @@ -24,9 +24,41 @@ function gutenberg_reregister_interactivity_script_modules() { wp_register_script_module( '@wordpress/interactivity-router', gutenberg_url( '/build-module/interactivity-router/index.min.js' ), - array( '@wordpress/interactivity' ), + array( + array( + 'id' => '@wordpress/a11y', + 'import' => 'dynamic', + ), + '@wordpress/interactivity', + ), $default_version ); } - add_action( 'init', 'gutenberg_reregister_interactivity_script_modules' ); + +/** + * Adds script data to the interactivity-router script module. + * + * This filter is registered conditionally anticipating a WordPress Core change to add the script module data. + * The filter runs on 'after_setup_theme' (when Core registers Interactivity and Script Modules hooks) + * to ensure that the conditional registration happens after Core and correctly determine whether + * the filter should be added. + * + * @see https://github.com/WordPress/wordpress-develop/pull/7304 + */ +function gutenberg_register_interactivity_script_module_data_hooks() { + if ( ! has_filter( 'script_module_data_@wordpress/interactivity-router', array( wp_interactivity(), 'filter_script_module_interactivity_router_data' ) ) ) { + add_filter( + 'script_module_data_@wordpress/interactivity-router', + function ( $data ) { + if ( ! isset( $data['i18n'] ) ) { + $data['i18n'] = array(); + } + $data['i18n']['loading'] = __( 'Loading page, please wait.', 'default' ); + $data['i18n']['loaded'] = __( 'Page Loaded.', 'default' ); + return $data; + } + ); + } +} +add_action( 'after_setup_theme', 'gutenberg_register_interactivity_script_module_data_hooks', 20 ); diff --git a/package-lock.json b/package-lock.json index 49d1ad6b123b7..dcdc5043ac6d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54329,6 +54329,7 @@ "version": "2.7.0", "license": "GPL-2.0-or-later", "dependencies": { + "@wordpress/a11y": "file:../a11y", "@wordpress/interactivity": "file:../interactivity" }, "engines": { @@ -69038,6 +69039,7 @@ "@wordpress/interactivity-router": { "version": "file:packages/interactivity-router", "requires": { + "@wordpress/a11y": "file:../a11y", "@wordpress/interactivity": "file:../interactivity" } }, diff --git a/packages/interactivity-router/package.json b/packages/interactivity-router/package.json index db85c0d8bdba3..7282ee0b00f9c 100644 --- a/packages/interactivity-router/package.json +++ b/packages/interactivity-router/package.json @@ -28,6 +28,7 @@ "types": "build-types", "wpScriptModuleExports": "./build-module/index.js", "dependencies": { + "@wordpress/a11y": "file:../a11y", "@wordpress/interactivity": "file:../interactivity" }, "publishConfig": { diff --git a/packages/interactivity-router/src/index.ts b/packages/interactivity-router/src/index.ts index c6e1087b038a5..3bd44c7aebd71 100644 --- a/packages/interactivity-router/src/index.ts +++ b/packages/interactivity-router/src/index.ts @@ -209,16 +209,37 @@ const isValidEvent = ( event: MouseEvent ) => // Variable to store the current navigation. let navigatingTo = ''; -export const { state, actions } = store( 'core/router', { +let hasLoadedNavigationTextsData = false; +const navigationTexts = { + loading: 'Loading page, please wait.', + loaded: 'Page Loaded.', +}; + +interface Store { + state: { + url: string; + navigation: { + hasStarted: boolean; + hasFinished: boolean; + message: string; + texts?: { + loading?: string; + loaded?: string; + }; + }; + }; + actions: { + navigate: ( href: string, options?: NavigateOptions ) => void; + prefetch: ( url: string, options?: PrefetchOptions ) => void; + }; +} + +export const { state, actions } = store< Store >( 'core/router', { state: { url: window.location.href, navigation: { hasStarted: false, hasFinished: false, - texts: { - loading: '', - loaded: '', - }, message: '', }, }, @@ -275,7 +296,7 @@ export const { state, actions } = store( 'core/router', { navigation.hasFinished = false; } if ( screenReaderAnnouncement ) { - navigation.message = navigation.texts.loading; + a11ySpeak( 'loading' ); } }, 400 ); @@ -315,14 +336,7 @@ export const { state, actions } = store( 'core/router', { } if ( screenReaderAnnouncement ) { - // Announce that the page has been loaded. If the message is the - // same, we use a no-break space similar to the @wordpress/a11y - // package: https://github.com/WordPress/gutenberg/blob/c395242b8e6ee20f8b06c199e4fc2920d7018af1/packages/a11y/src/filter-message.js#L20-L26 - navigation.message = - navigation.texts.loaded + - ( navigation.message === navigation.texts.loaded - ? '\u00A0' - : '' ); + a11ySpeak( 'loaded' ); } // Scroll to the anchor if exits in the link. @@ -363,6 +377,58 @@ export const { state, actions } = store( 'core/router', { }, } ); +/** + * Announces a message to screen readers. + * + * This is a wrapper around the `@wordpress/a11y` package's `speak` function. It handles importing + * the package on demand and should be used instead of calling `ally.speak` direacly. + * + * @param messageKey The message to be announced by assistive technologies. + */ +function a11ySpeak( messageKey: keyof typeof navigationTexts ) { + if ( ! hasLoadedNavigationTextsData ) { + hasLoadedNavigationTextsData = true; + const content = document.getElementById( + 'wp-script-module-data-@wordpress/interactivity-router' + )?.textContent; + if ( content ) { + try { + const parsed = JSON.parse( content ); + if ( typeof parsed?.i18n?.loading === 'string' ) { + navigationTexts.loading = parsed.i18n.loading; + } + if ( typeof parsed?.i18n?.loaded === 'string' ) { + navigationTexts.loaded = parsed.i18n.loaded; + } + } catch {} + } else { + // Fallback to localized strings from Interactivity API state. + if ( state.navigation.texts?.loading ) { + navigationTexts.loading = state.navigation.texts.loading; + } + if ( state.navigation.texts?.loaded ) { + navigationTexts.loaded = state.navigation.texts.loaded; + } + } + } + + const message = navigationTexts[ messageKey ]; + + if ( globalThis.IS_GUTENBERG_PLUGIN ) { + import( '@wordpress/a11y' ).then( + ( { speak } ) => speak( message ), + // Ignore failures to load the a11y module. + () => {} + ); + } else { + state.navigation.message = + // Announce that the page has been loaded. If the message is the + // same, we use a no-break space similar to the @wordpress/a11y + // package: https://github.com/WordPress/gutenberg/blob/c395242b8e6ee20f8b06c199e4fc2920d7018af1/packages/a11y/src/filter-message.js#L20-L26 + message + ( state.navigation.message === message ? '\u00A0' : '' ); + } +} + // Add click and prefetch to all links. if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( navigationMode === 'fullPage' ) { diff --git a/packages/interactivity-router/tsconfig.json b/packages/interactivity-router/tsconfig.json index eff80af2f28db..f601a26a86f5f 100644 --- a/packages/interactivity-router/tsconfig.json +++ b/packages/interactivity-router/tsconfig.json @@ -7,6 +7,6 @@ "checkJs": false, "strict": false }, - "references": [ { "path": "../interactivity" } ], + "references": [ { "path": "../a11y" }, { "path": "../interactivity" } ], "include": [ "src/**/*" ] }