Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactivity API: Allow async directive registration #60670

Draft
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "test/directive-function",
"title": "E2E Interactivity tests - directive function",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScriptModule": "file:./view.js",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/**
* HTML for testing the directive function.
*
* @package gutenberg-test-interactive-blocks
*/
?>

<div data-wp-interactive="directive-function">
<button
data-testid="async directive"
style="background-color:white"
data-wp-async-bg-color="state.colors"
>Click me!</button>

<button
data-testid="async directive with others"
style="background-color:white"
data-wp-async-bg-color="state.colors"
data-wp-context='{"count": 0}'
data-wp-text="context.count"
data-wp-on--click="actions.updateCount"
>0</button>

<button
data-testid="load async directive"
data-wp-on--click="actions.loadAsyncDirective"
>Load async directive</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array( 'dependencies' => array( '@wordpress/interactivity' ) );
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* WordPress dependencies
*/
import { store, getContext, useState, useCallback, privateApis } from '@wordpress/interactivity';

const { directive } = privateApis(
'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.'
);

store( 'directive-function', {
state: {
colors: [ "white", "magenta", "cyan", "yellow" ]
},
actions: {
updateCount: () => {
const context = getContext();
context.count += 1;
},
loadAsyncDirective: () => {
directive(
'async-bg-color',
( { directives: { 'async-bg-color': bgColor }, element, evaluate } ) => {
const [ index, setIndex ] = useState( 0 );
const entry = bgColor.find( ( { suffix } ) => suffix === 'default' );
const colors = evaluate( entry );

const clickHandler = useCallback(() => {
setIndex( ( index + 1 ) % colors.length );
}, [colors.length, index]);


// Use mouseup event so this doesn't interfere with `data-wp-on--click`.
element.props.onmouseup = clickHandler;
element.props.style = { backgroundColor: colors[index] };
}
);
}
},
} );
23 changes: 17 additions & 6 deletions packages/interactivity/src/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from 'preact';
import { useRef, useCallback, useContext } from 'preact/hooks';
import type { VNode, Context, RefObject } from 'preact';
import { deepSignal } from 'deepsignal';

/**
* Internal dependencies
Expand Down Expand Up @@ -173,7 +174,9 @@ export const resetNamespace = () => {
};

// WordPress Directives.
const directiveCallbacks: Record< string, DirectiveCallback > = {};
const directiveCallbacks: Record< string, DirectiveCallback > = deepSignal(
{}
);
const directivePriorities: Record< string, number > = {};

/**
Expand Down Expand Up @@ -312,11 +315,15 @@ const getPriorityLevels: GetPriorityLevels = ( directives ) => {
// Component that wraps each priority level of directives of an element.
const Directives = ( {
directives,
priorityLevels: [ currentPriorityLevel, ...nextPriorityLevels ],
priorityLevels,
element,
originalProps,
previousScope,
}: DirectivesProps ) => {
// Initialize the priority levels in the first Directives component.
const [ currentPriorityLevel = [], ...nextPriorityLevels ] =
priorityLevels ?? getPriorityLevels( directives );

// Initialize the scope of this element. These scopes are different per each
// level because each level has a different context, but they share the same
// element ref, state and props.
Expand Down Expand Up @@ -373,16 +380,20 @@ options.vnode = ( vnode: VNode< any > ) => {
if ( vnode.props.__directives ) {
const props = vnode.props;
const directives = props.__directives;
if ( directives.key )
if ( directives.key ) {
vnode.key = directives.key.find(
( { suffix } ) => suffix === 'default'
).value;
}
// Remove known directives withouth callback.
[ 'interactive', 'key' ].forEach( ( d ) => {
delete directives[ d ];
} );
delete props.__directives;
const priorityLevels = getPriorityLevels( directives );
if ( priorityLevels.length > 0 ) {

if ( Object.keys( directives ).length > 0 ) {
vnode.props = {
directives,
priorityLevels,
originalProps: props,
type: vnode.type,
element: createElement( vnode.type as any, props ),
Expand Down
98 changes: 98 additions & 0 deletions test/e2e/specs/interactivity/directive-function.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Internal dependencies
*/
import { test, expect } from './fixtures';

test.describe( 'directive function', () => {
test.beforeAll( async ( { interactivityUtils: utils } ) => {
await utils.activatePlugins();
await utils.addPostWithBlock( 'test/directive-function' );
} );

test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
await page.goto( utils.getLink( 'test/directive-function' ) );
} );

test.afterAll( async ( { interactivityUtils: utils } ) => {
await utils.deactivatePlugins();
await utils.deleteAllPosts();
} );

test( 'should support async directive registration', async ( { page } ) => {
const element = page.getByTestId( 'async directive' );
const button = page.getByTestId( 'load async directive' );

const colors = {
white: 'rgb(255, 255, 255)',
magenta: 'rgb(255, 0, 255)',
cyan: 'rgb(0, 255, 255)',
yellow: 'rgb(255, 255, 0)',
};

await expect( element ).toHaveCSS(
'background-color',
'rgb(255, 255, 255)'
);

// Clicking the element with the async directive changes the background,
// but it hasn't loaded yet. Nothing should happen.
await element.click( { clickCount: 3 } );
await expect( element ).toHaveCSS( 'background-color', colors.white );

// Load the async directive.
await button.click();

// The element should now change the background color when clicked.
await element.click();
await expect( element ).toHaveCSS( 'background-color', colors.magenta );
await element.click();
await expect( element ).toHaveCSS( 'background-color', colors.cyan );
await element.click();
await expect( element ).toHaveCSS( 'background-color', colors.yellow );
await element.click();
await expect( element ).toHaveCSS( 'background-color', colors.white );
} );

test( 'should support async directive along with other directives', async ( {
page,
} ) => {
const element = page.getByTestId( 'async directive with others' );
const button = page.getByTestId( 'load async directive' );

const colors = {
white: 'rgb(255, 255, 255)',
magenta: 'rgb(255, 0, 255)',
cyan: 'rgb(0, 255, 255)',
yellow: 'rgb(255, 255, 0)',
};

await expect( element ).toHaveText( '0' );
await expect( element ).toHaveCSS(
'background-color',
'rgb(255, 255, 255)'
);

// Clicking the element with the async directive changes the background,
// but it hasn't loaded yet. Only other directives should react.
await element.click( { clickCount: 3 } );
await expect( element ).toHaveText( '3' );
await expect( element ).toHaveCSS( 'background-color', colors.white );

// Load the async directive.
await button.click();

// The element should now change the background color when clicked.
await element.click();
await expect( element ).toHaveText( '4' );
await expect( element ).toHaveCSS( 'background-color', colors.magenta );
await element.click();
await expect( element ).toHaveText( '5' );
await expect( element ).toHaveCSS( 'background-color', colors.cyan );
await element.click();
await expect( element ).toHaveText( '6' );
await expect( element ).toHaveCSS( 'background-color', colors.yellow );
await element.click();
await expect( element ).toHaveText( '7' );
await expect( element ).toHaveCSS( 'background-color', colors.white );
} );
} );
Loading