diff --git a/src/js/_enqueues/wp/emoji.js b/src/js/_enqueues/wp/emoji.js
index a1f0f4face234..9ec1a788a95b0 100644
--- a/src/js/_enqueues/wp/emoji.js
+++ b/src/js/_enqueues/wp/emoji.js
@@ -110,7 +110,9 @@
// Emoji in a text node are replaced by parsing the element which contains it.
const node = addedNode instanceof Text ? addedNode.parentElement : addedNode;
- if ( node instanceof HTMLElement && test( node.textContent ) ) {
+ if (
+ node instanceof HTMLElement && test( node.textContent ) && ! isExcludedFromParsing( node )
+ ) {
parse( node );
}
}
@@ -123,6 +125,39 @@
parse( document.body );
}
+ /**
+ * Checks whether a node, or one of its ancestors, is excluded from
+ * emoji parsing.
+ *
+ * The doNotParse() callback passed to Twemoji only excludes descendants
+ * of the node it's asked to parse, so a node that is itself excluded
+ * (or nested inside an excluded ancestor) has to be checked here,
+ * before it's ever handed to parse().
+ *
+ * @since 7.2.0
+ * @private
+ *
+ * @param {HTMLElement|null} node The node to check.
+ *
+ * @return {boolean} Whether the node or an ancestor has the
+ * wp-exclude-emoji class.
+ */
+ function isExcludedFromParsing( node ) {
+ while ( node ) {
+ if (
+ node.className &&
+ typeof node.className === 'string' &&
+ node.className.indexOf( 'wp-exclude-emoji' ) !== -1
+ ) {
+ return true;
+ }
+
+ node = node.parentElement;
+ }
+
+ return false;
+ }
+
/**
* Tests if a text string contains emoji characters.
*
diff --git a/tests/qunit/wp-includes/js/emoji.html b/tests/qunit/wp-includes/js/emoji.html
new file mode 100644
index 0000000000000..049a6468c2f5a
--- /dev/null
+++ b/tests/qunit/wp-includes/js/emoji.html
@@ -0,0 +1,31 @@
+
+
+
+ wp.emoji QUnit Test Suite
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/qunit/wp-includes/js/emoji.js b/tests/qunit/wp-includes/js/emoji.js
new file mode 100644
index 0000000000000..597071079ee8c
--- /dev/null
+++ b/tests/qunit/wp-includes/js/emoji.js
@@ -0,0 +1,52 @@
+( function( QUnit ) {
+ var FLAG = '🇰🇷'; // Flag: South Korea.
+
+ QUnit.module( 'wp.emoji' );
+
+ function assertNotParsed( assert, container, done ) {
+ window.setTimeout( function() {
+ assert.strictEqual( container.querySelector( 'img.emoji' ), null, 'the excluded element should not contain an emoji image' );
+ container.remove();
+ done();
+ }, 50 );
+ }
+
+ QUnit.test( 'does not parse an excluded element appended whole after load', function( assert ) {
+ var done = assert.async(),
+ span = document.createElement( 'span' );
+
+ span.className = 'wp-exclude-emoji';
+ span.textContent = FLAG;
+ document.body.appendChild( span );
+
+ assertNotParsed( assert, span, done );
+ } );
+
+ QUnit.test( 'does not parse an excluded element whose text is replaced after load', function( assert ) {
+ var done = assert.async(),
+ span = document.createElement( 'span' );
+
+ span.className = 'wp-exclude-emoji';
+ document.body.appendChild( span );
+
+ window.setTimeout( function() {
+ span.textContent = FLAG;
+ assertNotParsed( assert, span, done );
+ }, 50 );
+ } );
+
+ QUnit.test( 'does not parse text appended inside an already-excluded ancestor', function( assert ) {
+ var done = assert.async(),
+ wrapper = document.createElement( 'span' ),
+ child = document.createElement( 'em' );
+
+ wrapper.className = 'wp-exclude-emoji';
+ wrapper.appendChild( child );
+ document.body.appendChild( wrapper );
+
+ window.setTimeout( function() {
+ child.textContent = FLAG;
+ assertNotParsed( assert, wrapper, done );
+ }, 50 );
+ } );
+} )( window.QUnit );