diff --git a/blocks/editable/format-toolbar/index.js b/blocks/editable/format-toolbar/index.js
index 023e2408974bdb..1e16b6f783970b 100644
--- a/blocks/editable/format-toolbar/index.js
+++ b/blocks/editable/format-toolbar/index.js
@@ -8,7 +8,7 @@ import { isUndefined } from 'lodash';
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
-import { IconButton, Toolbar } from '@wordpress/components';
+import { IconButton, Toolbar, withA11yMessages } from '@wordpress/components';
import { keycodes } from '@wordpress/utils';
/**
@@ -125,6 +125,12 @@ class FormatToolbar extends Component {
this.setState( {
isEditingLink: false,
} );
+ if (
+ this.props.formats.link.value === '' &&
+ !! this.state.linkValue.length
+ ) {
+ this.props.speak( __( 'Link inserted.' ), 'assertive' );
+ }
}
updateLinkValue( linkValue ) {
@@ -187,4 +193,4 @@ class FormatToolbar extends Component {
}
}
-export default FormatToolbar;
+export default withA11yMessages( FormatToolbar );
diff --git a/blocks/url-input/index.js b/blocks/url-input/index.js
index 98972d36484dfa..b410f8a95da4e3 100644
--- a/blocks/url-input/index.js
+++ b/blocks/url-input/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { throttle, debounce } from 'lodash';
+import { throttle } from 'lodash';
import classnames from 'classnames';
import scrollIntoView from 'dom-scroll-into-view';
@@ -11,7 +11,7 @@ import scrollIntoView from 'dom-scroll-into-view';
import { __, sprintf, _n } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { keycodes } from '@wordpress/utils';
-import { Spinner, withInstanceId } from '@wordpress/components';
+import { Spinner, withInstanceId, withA11yMessages } from '@wordpress/components';
const { UP, DOWN, ENTER } = keycodes;
@@ -22,7 +22,6 @@ class UrlInput extends Component {
this.onKeyDown = this.onKeyDown.bind( this );
this.bindListNode = this.bindListNode.bind( this );
this.updateSuggestions = throttle( this.updateSuggestions.bind( this ), 200 );
- this.debouncedSpeakAssertive = debounce( this.speakAssertive.bind( this ), 500 );
this.suggestionNodes = [];
this.state = {
posts: [],
@@ -77,13 +76,13 @@ class UrlInput extends Component {
} );
if ( !! posts.length ) {
- this.debouncedSpeakAssertive( sprintf( _n(
+ this.props.debouncedSpeak( sprintf( _n(
'%d result found, use up and down arrow keys to navigate.',
'%d results found, use up and down arrow keys to navigate.',
posts.length
- ), posts.length ) );
+ ), posts.length ), 'assertive' );
} else {
- this.debouncedSpeakAssertive( __( 'No results.' ) );
+ this.props.debouncedSpeak( __( 'No results.' ), 'assertive' );
}
},
( xhr ) => {
@@ -137,15 +136,10 @@ class UrlInput extends Component {
}
}
- speakAssertive( message ) {
- wp.a11y.speak( message, 'assertive' );
- }
-
componentWillUnmount() {
if ( this.suggestionsRequest ) {
this.suggestionsRequest.abort();
}
- this.debouncedSpeakAssertive.cancel();
}
componentDidUpdate() {
@@ -220,4 +214,4 @@ class UrlInput extends Component {
}
}
-export default withInstanceId( UrlInput );
+export default withA11yMessages( withInstanceId( UrlInput ) );
diff --git a/components/form-token-field/index.js b/components/form-token-field/index.js
index 03400386a5fc3a..c698d27e8e7f31 100644
--- a/components/form-token-field/index.js
+++ b/components/form-token-field/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { last, take, clone, uniq, map, difference, each, identity, some, debounce } from 'lodash';
+import { last, take, clone, uniq, map, difference, each, identity, some } from 'lodash';
import classnames from 'classnames';
/**
@@ -18,6 +18,7 @@ import Token from './token';
import TokenInput from './token-input';
import SuggestionsList from './suggestions-list';
import withInstanceId from '../higher-order/with-instance-id';
+import withA11yMessages from '../higher-order/with-a11y-messages';
const initialState = {
incompleteTokenValue: '',
@@ -46,7 +47,6 @@ class FormTokenField extends Component {
this.onInputChange = this.onInputChange.bind( this );
this.bindInput = this.bindInput.bind( this );
this.bindTokensAndInput = this.bindTokensAndInput.bind( this );
- this.debouncedSpeakAssertive = debounce( this.speakAssertive.bind( this ), 500 );
}
componentDidUpdate() {
@@ -64,10 +64,6 @@ class FormTokenField extends Component {
}
}
- componentWillUnmount() {
- this.debouncedSpeakAssertive.cancel();
- }
-
bindInput( ref ) {
this.input = ref;
}
@@ -197,13 +193,13 @@ class FormTokenField extends Component {
if ( showMessage ) {
const matchingSuggestions = this.getMatchingSuggestions( tokenValue );
if ( !! matchingSuggestions.length ) {
- this.debouncedSpeakAssertive( sprintf( _n(
+ this.props.debouncedSpeak( sprintf( _n(
'%d result found, use up and down arrow keys to navigate.',
'%d results found, use up and down arrow keys to navigate.',
matchingSuggestions.length
- ), matchingSuggestions.length ) );
+ ), matchingSuggestions.length ), 'assertive' );
} else {
- this.debouncedSpeakAssertive( __( 'No results.' ) );
+ this.props.debouncedSpeak( __( 'No results.' ), 'assertive' );
}
}
}
@@ -344,7 +340,7 @@ class FormTokenField extends Component {
addNewToken( token ) {
this.addNewTokens( [ token ] );
- this.speakAssertive( this.props.messages.added );
+ this.props.speak( this.props.messages.added, 'assertive' );
this.setState( {
incompleteTokenValue: '',
@@ -362,7 +358,7 @@ class FormTokenField extends Component {
return this.getTokenValue( item ) !== this.getTokenValue( token );
} );
this.props.onChange( newTokens );
- this.speakAssertive( this.props.messages.removed );
+ this.props.speak( this.props.messages.removed, 'assertive' );
}
getTokenValue( token ) {
@@ -406,10 +402,6 @@ class FormTokenField extends Component {
return take( suggestions, maxSuggestions );
}
- speakAssertive( message ) {
- wp.a11y.speak( message, 'assertive' );
- }
-
getSelectedSuggestion() {
if ( this.state.selectedSuggestionIndex !== -1 ) {
return this.getMatchingSuggestions()[ this.state.selectedSuggestionIndex ];
@@ -572,4 +564,4 @@ FormTokenField.defaultProps = {
},
};
-export default withInstanceId( FormTokenField );
+export default withA11yMessages( withInstanceId( FormTokenField ) );
diff --git a/components/higher-order/with-a11y-messages/index.js b/components/higher-order/with-a11y-messages/index.js
new file mode 100644
index 00000000000000..8ee02598e48b38
--- /dev/null
+++ b/components/higher-order/with-a11y-messages/index.js
@@ -0,0 +1,44 @@
+/**
+ * External dependencies
+ */
+import { debounce } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component } from 'element';
+
+/**
+ * A Higher Order Component used to be provide a unique instance ID by component
+ *
+ * @param {WPElement} WrappedComponent The wrapped component
+ *
+ * @return {Component} Component with an instanceId prop.
+ */
+function withA11yMessages( WrappedComponent ) {
+ return class extends Component {
+ constructor() {
+ super( ...arguments );
+ this.debouncedSpeak = debounce( this.speak.bind( this ), 500 );
+ }
+
+ speak( message, type = 'polite' ) {
+ wp.a11y.speak( message, type );
+ }
+
+ componentWillUnmount() {
+ this.debouncedSpeak.cancel();
+ }
+
+ render() {
+ return (
+