-
0
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js
index 1fc1e938972de6..11d01b7a216d1c 100644
--- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js
+++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js
@@ -1,7 +1,20 @@
/**
* WordPress dependencies
*/
-import { store } from '@wordpress/interactivity';
+import { store, directive, getContext } from '@wordpress/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': showMock }, element, evaluate } ) => {
+ const entry = showMock.find( ( { suffix } ) => suffix === 'default' );
+ if ( ! evaluate( entry ) ) {
+ return null;
+ }
+ return element;
+ }
+);
const { state } = store( 'directive-on-window', {
state: {
@@ -12,4 +25,10 @@ const { state } = store( 'directive-on-window', {
state.counter += 1;
},
},
+ actions: {
+ visibilityHandler: () => {
+ const context = getContext();
+ context.isVisible = ! context.isVisible;
+ },
+ }
} );
diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js
index 9ab51b0ba9bdb4..902925586645c4 100644
--- a/packages/interactivity/src/directives.js
+++ b/packages/interactivity/src/directives.js
@@ -8,7 +8,7 @@ import { deepSignal, peek } from 'deepsignal';
* Internal dependencies
*/
import { createPortal } from './portals';
-import { useWatch, useInit, useEffect } from './utils';
+import { useWatch, useInit } from './utils';
import { directive } from './hooks';
const isObject = ( item ) =>
@@ -28,6 +28,64 @@ const mergeDeepSignals = ( target, source, overwrite ) => {
}
};
+const newRule =
+ /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g;
+const ruleClean = /\/\*[^]*?\*\/| +/g;
+const ruleNewline = /\n+/g;
+const empty = ' ';
+
+/**
+ * Convert a css style string into a object.
+ *
+ * Made by Cristian Bote (@cristianbote) for Goober.
+ * https://unpkg.com/browse/goober@2.1.13/src/core/astish.js
+ *
+ * @param {string} val CSS string.
+ * @return {Object} CSS object.
+ */
+const cssStringToObject = ( val ) => {
+ const tree = [ {} ];
+ let block, left;
+
+ while ( ( block = newRule.exec( val.replace( ruleClean, '' ) ) ) ) {
+ if ( block[ 4 ] ) {
+ tree.shift();
+ } else if ( block[ 3 ] ) {
+ left = block[ 3 ].replace( ruleNewline, empty ).trim();
+ tree.unshift( ( tree[ 0 ][ left ] = tree[ 0 ][ left ] || {} ) );
+ } else {
+ tree[ 0 ][ block[ 1 ] ] = block[ 2 ]
+ .replace( ruleNewline, empty )
+ .trim();
+ }
+ }
+
+ return tree[ 0 ];
+};
+
+/**
+ * Creates a directive that adds an event listener to the global window or
+ * document object.
+ *
+ * @param {string} type 'window' or 'document'
+ * @return {void}
+ */
+const getGlobalEventDirective =
+ ( type ) =>
+ ( { directives, evaluate } ) => {
+ directives[ `on-${ type }` ]
+ .filter( ( { suffix } ) => suffix !== 'default' )
+ .forEach( ( entry ) => {
+ useInit( () => {
+ const cb = ( event ) => evaluate( entry, event );
+ const globalVar = type === 'window' ? window : document;
+ globalVar.addEventListener( entry.suffix, cb );
+ return () =>
+ globalVar.removeEventListener( entry.suffix, cb );
+ }, [] );
+ } );
+ };
+
export default () => {
// data-wp-context
directive(
@@ -87,22 +145,6 @@ export default () => {
} );
} );
- const getGlobalEventDirective =
- ( type ) =>
- ( { directives, evaluate } ) => {
- directives[ `on-${ type }` ]
- .filter( ( { suffix } ) => suffix !== 'default' )
- .forEach( ( entry ) => {
- useEffect( () => {
- const cb = ( event ) => evaluate( entry, event );
- const globalVar = type === 'window' ? window : document;
- globalVar.addEventListener( entry.suffix, cb );
- return () =>
- globalVar.removeEventListener( entry.suffix, cb );
- }, [] );
- } );
- };
-
// data-wp-on-window--[event]
directive( 'on-window', getGlobalEventDirective( 'window' ) );
// data-wp-on-document--[event]
@@ -145,41 +187,6 @@ export default () => {
}
);
- const newRule =
- /(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}\s*)/g;
- const ruleClean = /\/\*[^]*?\*\/| +/g;
- const ruleNewline = /\n+/g;
- const empty = ' ';
-
- /**
- * Convert a css style string into a object.
- *
- * Made by Cristian Bote (@cristianbote) for Goober.
- * https://unpkg.com/browse/goober@2.1.13/src/core/astish.js
- *
- * @param {string} val CSS string.
- * @return {Object} CSS object.
- */
- const cssStringToObject = ( val ) => {
- const tree = [ {} ];
- let block, left;
-
- while ( ( block = newRule.exec( val.replace( ruleClean, '' ) ) ) ) {
- if ( block[ 4 ] ) {
- tree.shift();
- } else if ( block[ 3 ] ) {
- left = block[ 3 ].replace( ruleNewline, empty ).trim();
- tree.unshift( ( tree[ 0 ][ left ] = tree[ 0 ][ left ] || {} ) );
- } else {
- tree[ 0 ][ block[ 1 ] ] = block[ 2 ]
- .replace( ruleNewline, empty )
- .trim();
- }
- }
-
- return tree[ 0 ];
- };
-
// data-wp-style--[style-key]
directive( 'style', ( { directives: { style }, element, evaluate } ) => {
style
diff --git a/test/e2e/specs/interactivity/directive-on-document.spec.ts b/test/e2e/specs/interactivity/directive-on-document.spec.ts
index e7333a98419381..918f3945e010f1 100644
--- a/test/e2e/specs/interactivity/directive-on-document.spec.ts
+++ b/test/e2e/specs/interactivity/directive-on-document.spec.ts
@@ -26,4 +26,20 @@ test.describe( 'data-wp-on-document', () => {
await page.keyboard.press( 'ArrowDown' );
await expect( counter ).toHaveText( '1' );
} );
+ test( 'the event listener is removed when the element is removed', async ( {
+ page,
+ } ) => {
+ const counter = page.getByTestId( 'counter' );
+ const visibilityButton = page.getByTestId( 'visibility' );
+ await expect( counter ).toHaveText( '0' );
+ await page.keyboard.press( 'ArrowDown' );
+ await expect( counter ).toHaveText( '1' );
+ // Remove the element.
+ await visibilityButton.click();
+ // This keyboard press should not increase the counter.
+ await page.keyboard.press( 'ArrowDown' );
+ // Add the element back.
+ await visibilityButton.click();
+ await expect( counter ).toHaveText( '1' );
+ } );
} );
diff --git a/test/e2e/specs/interactivity/directive-on-window.spec.ts b/test/e2e/specs/interactivity/directive-on-window.spec.ts
index 991a44586c7e1e..ff6abf04971b58 100644
--- a/test/e2e/specs/interactivity/directive-on-window.spec.ts
+++ b/test/e2e/specs/interactivity/directive-on-window.spec.ts
@@ -25,4 +25,19 @@ test.describe( 'data-wp-on-window', () => {
const counter = page.getByTestId( 'counter' );
await expect( counter ).toHaveText( '1' );
} );
+ test( 'the event listener is removed when the element is removed', async ( {
+ page,
+ } ) => {
+ const counter = page.getByTestId( 'counter' );
+ const visibilityButton = page.getByTestId( 'visibility' );
+ await page.setViewportSize( { width: 600, height: 600 } );
+ await expect( counter ).toHaveText( '1' );
+ // Remove the element.
+ await visibilityButton.click();
+ // This resize should not increase the counter.
+ await page.setViewportSize( { width: 300, height: 600 } );
+ // Add the element back.
+ await visibilityButton.click();
+ await expect( counter ).toHaveText( '1' );
+ } );
} );