diff --git a/.wp-env.json b/.wp-env.json
index 9fcd523a6a..a03d020903 100644
--- a/.wp-env.json
+++ b/.wp-env.json
@@ -47,7 +47,8 @@
"wp-content/plugins/multiple-required-features.php": "./tests/e2e/src/wordpress-files/test-plugins/multiple-required-features.php",
"wp-content/uploads/content-example.xml": "./tests/e2e/src/wordpress-files/test-docs/content-example.xml",
"wp-content/plugins/echo-shortcode.php": "./tests/e2e/src/wordpress-files/test-plugins/echo-shortcode.php",
- "wp-content/plugins/simulate-setting-dependency.php": "./tests/e2e/src/wordpress-files/test-plugins/simulate-setting-dependency.php"
+ "wp-content/plugins/enable-did-you-mean-block.php": "./tests/e2e/src/wordpress-files/test-plugins/enable-did-you-mean-block.php",
+ "wp-content/plugins/simulate-setting-dependency.php": "./tests/e2e/src/wordpress-files/test-plugins/simulate-setting-dependency.php"
}
}
}
diff --git a/assets/js/blocks/did-you-mean/Edit.js b/assets/js/blocks/did-you-mean/Edit.js
new file mode 100644
index 0000000000..70a87a02b0
--- /dev/null
+++ b/assets/js/blocks/did-you-mean/Edit.js
@@ -0,0 +1,27 @@
+/**
+ * WordPress dependencies.
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+import { createInterpolateElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Edit component.
+ *
+ * @returns {Function} Component.
+ */
+const Edit = () => {
+ const blockProps = useBlockProps({
+ className: 'wp-block-elasticpress-did-you-mean',
+ });
+
+ return (
+
+ {createInterpolateElement(__('Did you mean
Hello?', 'elasticpress'), {
+ a:
, // eslint-disable-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label, jsx-a11y/anchor-is-valid
+ })}
+
+ );
+};
+
+export default Edit;
diff --git a/assets/js/blocks/did-you-mean/block.json b/assets/js/blocks/did-you-mean/block.json
new file mode 100644
index 0000000000..0edc395cdd
--- /dev/null
+++ b/assets/js/blocks/did-you-mean/block.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "elasticpress/did-you-mean",
+ "title": "Did You Mean",
+ "category": "elasticpress",
+ "description": "Display ElasticPress spelling suggestions for search queries.",
+ "textdomain": "elasticpress",
+ "editorScript": "ep-did-you-mean-block-script"
+}
diff --git a/assets/js/blocks/did-you-mean/index.js b/assets/js/blocks/did-you-mean/index.js
new file mode 100644
index 0000000000..7d30205142
--- /dev/null
+++ b/assets/js/blocks/did-you-mean/index.js
@@ -0,0 +1,17 @@
+/**
+ * WordPress dependencies.
+ */
+import { registerBlockType } from '@wordpress/blocks';
+import { search } from '@wordpress/icons';
+
+/**
+ * Internal dependencies.
+ */
+import Edit from './Edit';
+import { name } from './block.json';
+
+registerBlockType(name, {
+ icon: search,
+ edit: () => ,
+ save: () => null,
+});
diff --git a/includes/classes/Feature/DidYouMean/DidYouMean.php b/includes/classes/Feature/DidYouMean/DidYouMean.php
index 8c084d990f..649d4a187f 100644
--- a/includes/classes/Feature/DidYouMean/DidYouMean.php
+++ b/includes/classes/Feature/DidYouMean/DidYouMean.php
@@ -8,7 +8,7 @@
namespace ElasticPress\Feature\DidYouMean;
-use ElasticPress\{Elasticsearch, Feature, FeatureRequirementsStatus, Features };
+use ElasticPress\{Elasticsearch, Feature, FeatureRequirementsStatus, Utils };
/**
* Did You Mean feature class.
@@ -61,6 +61,91 @@ public function setup() {
add_filter( 'ep_integrate_search_queries', [ $this, 'set_ep_suggestion' ], 10, 2 );
add_action( 'template_redirect', [ $this, 'automatically_redirect_user' ] );
add_action( 'ep_suggestions', [ $this, 'the_output' ] );
+
+ if ( $this->is_block_enabled_in_editor() ) {
+ add_action( 'init', [ $this, 'register_block' ] );
+ }
+ }
+
+ /**
+ * Check if Did You Mean block should be enabled in the editor.
+ *
+ * @since 5.4.0
+ * @return bool
+ */
+ protected function is_block_enabled_in_editor(): bool {
+ global $pagenow;
+
+ $in_editor = in_array( $pagenow, [ 'post-new.php', 'post.php' ], true );
+
+ /**
+ * Filter if Did You Mean block should be enabled in the post editor.
+ *
+ * @since 5.4.0
+ * @hook ep_did_you_mean_enabled_in_editor
+ * @param {bool} $enabled If enabled or not.
+ * @return {bool} If enabled or not.
+ */
+ return ! ( $in_editor && ! apply_filters( 'ep_did_you_mean_enabled_in_editor', false ) );
+ }
+
+ /**
+ * Register Did You Mean block.
+ *
+ * @return void
+ * @since 5.4.0
+ */
+ public function register_block(): void {
+ /**
+ * Registering it here so translation works
+ *
+ * @see https://core.trac.wordpress.org/ticket/54797#comment:20
+ */
+ wp_register_script(
+ 'ep-did-you-mean-block-script',
+ EP_URL . 'dist/js/did-you-mean-block-script.js',
+ Utils\get_asset_info( 'did-you-mean-block-script', 'dependencies' ),
+ Utils\get_asset_info( 'did-you-mean-block-script', 'version' ),
+ true
+ );
+
+ wp_set_script_translations( 'ep-did-you-mean-block-script', 'elasticpress' );
+
+ register_block_type_from_metadata(
+ EP_PATH . 'assets/js/blocks/did-you-mean',
+ [
+ 'render_callback' => [ $this, 'render_block' ],
+ ]
+ );
+ }
+
+ /**
+ * Render Did You Mean block.
+ *
+ * @param array $attributes Block attributes.
+ * @return string
+ * @since 5.4.0
+ */
+ public function render_block( array $attributes = [] ): string {
+ ob_start();
+ do_action( 'ep_suggestions' );
+ $output = ob_get_clean();
+
+ if ( empty( $output ) ) {
+ return '';
+ }
+
+ $wrapper_attributes = get_block_wrapper_attributes(
+ [
+ 'class' => 'wp-block-elasticpress-did-you-mean',
+ ]
+ );
+
+ return sprintf(
+ '%2$s
',
+ wp_kses_data( $wrapper_attributes ),
+ wp_kses_post( $output )
+ );
}
/**
diff --git a/package.json b/package.json
index e98acea487..eb53b96e0d 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"comments-script": "./assets/js/comments.js",
"comments-block-script": "./assets/js/blocks/comments/index.js",
"dashboard-script": "./assets/js/dashboard.js",
+ "did-you-mean-block-script": "./assets/js/blocks/did-you-mean/index.js",
"facets-script": "./assets/js/facets.js",
"facets-block-script": "./assets/js/blocks/facets/taxonomy/index.js",
"facets-date-block-script": "./assets/js/blocks/facets/date/index.js",
diff --git a/tests/e2e/src/specs/did-you-mean.spec.ts b/tests/e2e/src/specs/did-you-mean.spec.ts
new file mode 100644
index 0000000000..68463c92eb
--- /dev/null
+++ b/tests/e2e/src/specs/did-you-mean.spec.ts
@@ -0,0 +1,49 @@
+import { test, expect } from '../fixtures.js';
+import {
+ activatePlugin,
+ deactivatePlugin,
+ getEditorFrame,
+ goToAdminPage,
+ maybeEnableFeature,
+} from '../utils.js';
+import {
+ closeBlockInserter,
+ getBlocksList,
+ insertBlock,
+ openBlockInserter,
+} from '../block-editor.js';
+
+test.describe('Did You Mean Feature', { tag: '@group2' }, () => {
+ test.beforeAll(async () => {
+ await maybeEnableFeature('did-you-mean');
+ });
+
+ test.beforeEach(async ({ loggedInPage }) => {
+ await activatePlugin(loggedInPage, 'enable-did-you-mean-block', 'wpCli');
+ });
+
+ test.afterEach(async ({ loggedInPage }) => {
+ await deactivatePlugin(loggedInPage, 'enable-did-you-mean-block', 'wpCli');
+ });
+
+ /**
+ * The block is gated behind the `ep_did_you_mean_enabled_in_editor` filter
+ * in the post editor (it is intended for the FSE Site Editor). The test
+ * plugin flips that filter so we can exercise the inserter and the
+ * placeholder rendering without spinning up a block theme.
+ */
+ test('Can insert the Did You Mean block and see its placeholder', async ({ loggedInPage }) => {
+ await goToAdminPage(loggedInPage, 'post-new.php');
+
+ await openBlockInserter(loggedInPage);
+ await expect(await getBlocksList(loggedInPage)).toContainText('Did You Mean');
+ await insertBlock(loggedInPage, 'Did You Mean');
+ await closeBlockInserter(loggedInPage);
+
+ const editorFrame = await getEditorFrame(loggedInPage);
+ const block = editorFrame.locator('.wp-block.wp-block-elasticpress-did-you-mean').first();
+
+ await expect(block).toBeVisible();
+ await expect(block).toContainText('Did you mean Hello?');
+ });
+});
diff --git a/tests/e2e/src/wordpress-files/test-plugins/enable-did-you-mean-block.php b/tests/e2e/src/wordpress-files/test-plugins/enable-did-you-mean-block.php
new file mode 100644
index 0000000000..db9e34e01c
--- /dev/null
+++ b/tests/e2e/src/wordpress-files/test-plugins/enable-did-you-mean-block.php
@@ -0,0 +1,12 @@
+assertStringContainsString( 'Showing results for: teet', $output );
}
+ /**
+ * Test Did You Mean block rendering.
+ */
+ public function testRenderBlock() {
+ global $wp_the_query, $wp_query;
+
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $query = new \WP_Query(
+ [
+ 's' => 'teet',
+ ]
+ );
+
+ $wp_the_query = $query;
+ $wp_query = $query;
+
+ $feature = ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' );
+ $feature->register_block();
+
+ $blocks = parse_blocks( '' );
+ $output = render_block( $blocks[0] );
+
+ $expected = sprintf( 'Did you mean: test?', get_search_link( 'test' ) );
+
+ $this->assertStringContainsString( 'wp-block-elasticpress-did-you-mean', $output );
+ $this->assertStringContainsString( $expected, $output );
+ }
+
+ /**
+ * Test Did You Mean block rendering when no suggestion is available.
+ */
+ public function testRenderBlockWithoutSuggestion() {
+ global $wp_the_query, $wp_query;
+
+ $this->ep_factory->post->create( [ 'post_content' => 'Test post' ] );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $query = new \WP_Query(
+ [
+ 's' => 'test',
+ ]
+ );
+
+ $wp_the_query = $query;
+ $wp_query = $query;
+
+ $output = ElasticPress\Features::factory()->get_registered_feature( 'did-you-mean' )->render_block();
+
+ $this->assertSame( '', $output );
+ }
+
/**
* Test maybe_sanitize_suggestion method.
*/