diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json
new file mode 100644
index 00000000000000..61b48396be08ae
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json
@@ -0,0 +1,14 @@
+{
+ "apiVersion": 2,
+ "name": "test/directive-body",
+ "title": "E2E Interactivity tests - directive body",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewScript": "directive-body-view",
+ "render": "file:./render.php"
+}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-body/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-body/render.php
new file mode 100644
index 00000000000000..5e24b7d7a3b9b5
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-body/render.php
@@ -0,0 +1,22 @@
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-body/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-body/view.js
new file mode 100644
index 00000000000000..f3cbc521f4355b
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-body/view.js
@@ -0,0 +1,11 @@
+( ( { wp } ) => {
+ const { store } = wp.interactivity;
+
+ store( {
+ actions: {
+ toggleText: ( { context } ) => {
+ context.text = context.text === 'text-1' ? 'text-2' : 'text-1';
+ },
+ },
+ } );
+} )( window );
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-init/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-init/block.json
new file mode 100644
index 00000000000000..a7e195d2e4884a
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-init/block.json
@@ -0,0 +1,14 @@
+{
+ "apiVersion": 2,
+ "name": "test/directive-init",
+ "title": "E2E Interactivity tests - directive init",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewScript": "directive-init-view",
+ "render": "file:./render.php"
+}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-init/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-init/render.php
new file mode 100644
index 00000000000000..76d5b776a68bb3
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-init/render.php
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+ Initially visible
+
+
+
+ true
+
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js
new file mode 100644
index 00000000000000..274809df3a9e5c
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-init/view.js
@@ -0,0 +1,64 @@
+( ( { wp } ) => {
+ const { store, directive, useContext } = wp.interactivity;
+
+ // Mock `data-wp-show` directive to test when things are removed from the
+ // DOM. Replace with `data-wp-show` when it's ready.
+ directive(
+ 'show-mock',
+ ( {
+ directives: {
+ 'show-mock': { default: showMock },
+ },
+ element,
+ evaluate,
+ context,
+ } ) => {
+ const contextValue = useContext( context );
+ if ( ! evaluate( showMock, { context: contextValue } ) ) {
+ return null;
+ }
+ return element;
+ }
+ );
+
+
+ store( {
+ selector: {
+ isReady: ({ context: { isReady } }) => {
+ return isReady
+ .map(v => v ? 'true': 'false')
+ .join(',');
+ },
+ calls: ({ context: { calls } }) => {
+ return calls.join(',');
+ },
+ isMounted: ({ context }) => {
+ return context.isMounted ? 'true' : 'false';
+ },
+ },
+ actions: {
+ initOne: ( { context: { isReady, calls } } ) => {
+ isReady[0] = true;
+ // Subscribe to changes in that prop.
+ isReady[0] = isReady[0];
+ calls[0]++;
+ },
+ initTwo: ( { context: { isReady, calls } } ) => {
+ isReady[1] = true;
+ calls[1]++;
+ },
+ initMount: ( { context } ) => {
+ context.isMounted = true;
+ return () => {
+ context.isMounted = false;
+ }
+ },
+ reset: ( { context: { isReady } } ) => {
+ isReady.fill(false);
+ },
+ toggle: ( { context } ) => {
+ context.isVisible = ! context.isVisible;
+ },
+ },
+ } );
+} )( window );
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-on/block.json
new file mode 100644
index 00000000000000..b9d8aa5f9ce57d
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on/block.json
@@ -0,0 +1,14 @@
+{
+ "apiVersion": 2,
+ "name": "test/directive-on",
+ "title": "E2E Interactivity tests - directive on",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "supports": {
+ "interactivity": true
+ },
+ "textdomain": "e2e-interactivity",
+ "viewScript": "directive-on-view",
+ "render": "file:./render.php"
+}
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-on/render.php
new file mode 100644
index 00000000000000..9d96c7768a4894
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on/render.php
@@ -0,0 +1,52 @@
+
+
+
+
+
+
0
+
+
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on/view.js
new file mode 100644
index 00000000000000..c93b4d5ed34e63
--- /dev/null
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on/view.js
@@ -0,0 +1,27 @@
+( ( { wp } ) => {
+ const { store } = wp.interactivity;
+
+ store( {
+ state: {
+ counter: 0,
+ text: ''
+ },
+ actions: {
+ clickHandler: ( { state, event } ) => {
+ state.counter += 1;
+ event.target.dispatchEvent(
+ new CustomEvent( 'customevent', { bubbles: true } )
+ );
+ },
+ inputHandler: ( { state, event } ) => {
+ state.text = event.target.value;
+ },
+ selectHandler: ( { context, event } ) => {
+ context.option = event.target.value;
+ },
+ customEventHandler: ({ context }) => {
+ context.customEvents += 1;
+ },
+ },
+ } );
+} )( window );
diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js
index a25e83c170e8d0..16789bd9da0522 100644
--- a/packages/interactivity/src/directives.js
+++ b/packages/interactivity/src/directives.js
@@ -55,13 +55,8 @@ export default () => {
);
// data-wp-body
- directive( 'body', ( { props: { children }, context: inherited } ) => {
- const { Provider } = inherited;
- const inheritedValue = useContext( inherited );
- return createPortal(
- { children },
- document.body
- );
+ directive( 'body', ( { props: { children } } ) => {
+ return createPortal( children, document.body );
} );
// data-wp-effect--[name]
diff --git a/test/e2e/specs/interactivity/directive-init.spec.ts b/test/e2e/specs/interactivity/directive-init.spec.ts
new file mode 100644
index 00000000000000..aa81ab1ea61db2
--- /dev/null
+++ b/test/e2e/specs/interactivity/directive-init.spec.ts
@@ -0,0 +1,76 @@
+/**
+ * Internal dependencies
+ */
+import { test, expect } from './fixtures';
+
+test.describe( 'data-wp-init', () => {
+ test.beforeAll( async ( { interactivityUtils: utils } ) => {
+ await utils.activatePlugins();
+ await utils.addPostWithBlock( 'test/directive-init' );
+ } );
+
+ test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
+ await page.goto( utils.getLink( 'test/directive-init' ) );
+ } );
+
+ test.afterAll( async ( { interactivityUtils: utils } ) => {
+ await utils.deactivatePlugins();
+ await utils.deleteAllPosts();
+ } );
+
+ test( 'should run when the block renders', async ( { page } ) => {
+ const el = page.getByTestId( 'single init' );
+ await expect( el.getByTestId( 'isReady' ) ).toHaveText( 'true' );
+ await expect( el.getByTestId( 'calls' ) ).toHaveText( '1' );
+ } );
+
+ test( 'should not run again if accessed signals change', async ( {
+ page,
+ } ) => {
+ const el = page.getByTestId( 'single init' );
+ await expect( el.getByTestId( 'isReady' ) ).toHaveText( 'true' );
+ await el.getByRole( 'button' ).click();
+ await expect( el.getByTestId( 'isReady' ) ).toHaveText( 'false' );
+ await expect( el.getByTestId( 'calls' ) ).toHaveText( '1' );
+ } );
+
+ test( 'should run multiple inits if defined', async ( { page } ) => {
+ const el = page.getByTestId( 'multiple inits' );
+ await expect( el.getByTestId( 'isReady' ) ).toHaveText( 'true,true' );
+ await expect( el.getByTestId( 'calls' ) ).toHaveText( '1,1' );
+ } );
+
+ test( 'should run the init callback when the element is unmounted', async ( {
+ page,
+ } ) => {
+ const container = page.getByTestId( 'init show' );
+ const show = container.getByTestId( 'show' );
+ const toggle = container.getByTestId( 'toggle' );
+ const isMounted = container.getByTestId( 'isMounted' );
+
+ await expect( show ).toHaveText( 'Initially visible' );
+ await expect( isMounted ).toHaveText( 'true' );
+
+ await toggle.click();
+
+ await expect( show ).not.toBeVisible();
+ await expect( isMounted ).toHaveText( 'false' );
+ } );
+
+ test( 'should run init when the element is mounted', async ( { page } ) => {
+ const container = page.getByTestId( 'init show' );
+ const show = container.getByTestId( 'show' );
+ const toggle = container.getByTestId( 'toggle' );
+ const isMounted = container.getByTestId( 'isMounted' );
+
+ await toggle.click();
+
+ await expect( show ).not.toBeVisible();
+ await expect( isMounted ).toHaveText( 'false' );
+
+ await toggle.click();
+
+ await expect( show ).toHaveText( 'Initially visible' );
+ await expect( isMounted ).toHaveText( 'true' );
+ } );
+} );
diff --git a/test/e2e/specs/interactivity/directive-on.spec.ts b/test/e2e/specs/interactivity/directive-on.spec.ts
new file mode 100644
index 00000000000000..03dfe64462c567
--- /dev/null
+++ b/test/e2e/specs/interactivity/directive-on.spec.ts
@@ -0,0 +1,54 @@
+/**
+ * Internal dependencies
+ */
+import { test, expect } from './fixtures';
+
+test.describe( 'data-wp-on', () => {
+ test.beforeAll( async ( { interactivityUtils: utils } ) => {
+ await utils.activatePlugins();
+ await utils.addPostWithBlock( 'test/directive-on' );
+ } );
+
+ test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
+ await page.goto( utils.getLink( 'test/directive-on' ) );
+ } );
+
+ test.afterAll( async ( { interactivityUtils: utils } ) => {
+ await utils.deactivatePlugins();
+ await utils.deleteAllPosts();
+ } );
+
+ test( 'callbacks should run whenever the specified event is dispatched', async ( {
+ page,
+ } ) => {
+ const counter = page.getByTestId( 'counter' );
+ await page
+ .getByTestId( 'button' )
+ .click( { clickCount: 3, delay: 100 } );
+ await expect( counter ).toHaveText( '3' );
+ } );
+
+ test( 'callbacks should receive the dispatched event', async ( {
+ page,
+ } ) => {
+ const text = page.getByTestId( 'text' );
+ await page.getByTestId( 'input' ).fill( 'hello!' );
+ await expect( text ).toHaveText( 'hello!' );
+ } );
+
+ test( 'callbacks should be able to access the context', async ( {
+ page,
+ } ) => {
+ const option = page.getByTestId( 'option' );
+ await page.getByTestId( 'select' ).selectOption( 'dog' );
+ await expect( option ).toHaveText( 'dog' );
+ } );
+
+ test( 'should work with custom events', async ( { page } ) => {
+ const counter = page.getByTestId( 'custom events counter' );
+ await page
+ .getByTestId( 'custom events button' )
+ .click( { clickCount: 3, delay: 100 } );
+ await expect( counter ).toHaveText( '3' );
+ } );
+} );
diff --git a/test/e2e/specs/interactivity/directives-body.spec.ts b/test/e2e/specs/interactivity/directives-body.spec.ts
new file mode 100644
index 00000000000000..be11cfc556b591
--- /dev/null
+++ b/test/e2e/specs/interactivity/directives-body.spec.ts
@@ -0,0 +1,47 @@
+/**
+ * Internal dependencies
+ */
+import { test, expect } from './fixtures';
+
+test.describe( 'data-wp-body', () => {
+ test.beforeAll( async ( { interactivityUtils: utils } ) => {
+ await utils.activatePlugins();
+ await utils.addPostWithBlock( 'test/directive-body' );
+ } );
+
+ test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
+ await page.goto( utils.getLink( 'test/directive-body' ) );
+ } );
+
+ test.afterAll( async ( { interactivityUtils: utils } ) => {
+ await utils.deactivatePlugins();
+ await utils.deleteAllPosts();
+ } );
+
+ test( "should move the element to the document's body", async ( {
+ page,
+ } ) => {
+ const container = page.getByTestId( 'container' );
+ const parentTag = page
+ .getByTestId( 'element with data-wp-body' )
+ .locator( 'xpath=..' );
+
+ await expect( container ).toBeEmpty();
+ await expect( parentTag ).toHaveJSProperty( 'tagName', 'BODY' );
+ } );
+
+ test( 'should make context accessible for inner elements', async ( {
+ page,
+ } ) => {
+ const text = page
+ .getByTestId( 'element with data-wp-body' )
+ .getByTestId( 'text' );
+ const toggle = page.getByTestId( 'toggle text' );
+
+ await expect( text ).toHaveText( 'text-1' );
+ await toggle.click();
+ await expect( text ).toHaveText( 'text-2' );
+ await toggle.click();
+ await expect( text ).toHaveText( 'text-1' );
+ } );
+} );