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

Add runtime support for the wp-style directive #52645

Merged
merged 11 commits into from
Jul 18, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"apiVersion": 2,
luisherranz marked this conversation as resolved.
Show resolved Hide resolved
"name": "test/directive-style",
"title": "E2E Interactivity tests - directive style",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScript": "directive-style-view",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php
/**
* HTML for testing the directive `data-wp-style`.
*
* @package gutenberg-test-interactive-blocks
*/

?>

<div data-wp-interactive>
<button
data-wp-on--click="actions.toggleColor"
data-testid="toggle color"
>
Toggle Color
</button>

<button
data-wp-on--click="actions.switchColorToFalse"
data-testid="switch color to false"
>
Switch Color to False
</button>

<div
style="color: red; background: green;"
data-wp-style--color="state.color"
data-testid="dont change style if callback returns same value on hydration"
>Don't change style if callback returns same value on hydration</div>

<div
style="color: blue; background: green;"
data-wp-style--color="state.falseValue"
data-testid="remove style if callback returns falsy value on hydration"
>Remove style if callback returns falsy value on hydration</div>

<div
style="color: blue; background: green;"
data-wp-style--color="state.color"
data-testid="change style if callback returns a new value on hydration"
>Change style if callback returns a new value on hydration</div>

<div
style="color: blue; background: green; border: 1px solid black"
data-wp-style--color="state.falseValue"
data-wp-style--background="state.color"
data-wp-style--border="state.border"
data-testid="handles multiple styles and callbacks on hydration"
>Handles multiple styles and callbacks on hydration</div>

<div
data-wp-style--color="state.color"
data-testid="can add style when style attribute is missing on hydration"
>Can add style when style attribute is missing on hydration</div>

<div
style="color: red;"
data-wp-style--color="state.color"
data-testid="can toggle style"
>Can toggle style</div>

<div
style="color: red;"
data-wp-style--color="state.color"
data-testid="can remove style"
>Can remove style</div>

<div
style="color: blue; background: green; border: 1px solid black;"
data-wp-style--background="state.color"
data-testid="can toggle style in the middle"
>Can toggle style in the middle</div>

<div
style="background-color: green;"
data-wp-style--background-color="state.color"
data-testid="handles styles names with hyphens"
>Handles styles names with hyphens</div>

<div data-wp-context='{ "color": "blue" }'>
<div
style="color: blue;"
data-wp-style--color="context.color"
data-testid="can use context values"
></div>
<button
data-wp-on--click="actions.toggleContext"
data-testid="toggle context"
>
Toggle context
</button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
( ( { wp } ) => {
const { store } = wp.interactivity;

store( {
state: {
falseValue: false,
color: "red",
border: "2px solid yellow"
},
actions: {
toggleColor: ( { state } ) => {
state.color = state.color === "red" ? "blue" : "red";
},
switchColorToFalse: ({ state }) => {
state.color = false;
},
toggleContext: ( { context } ) => {
context.color = context.color === "red" ? "blue" : "red";
},
},
} );
} )( window );
7 changes: 7 additions & 0 deletions packages/interactivity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. -->

## Unreleased

### New Features

- Runtime support for the `data-wp-style` directive. ([#52645](https://github.com/WordPress/gutenberg/pull/52645))
69 changes: 69 additions & 0 deletions packages/interactivity/src/directives.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,75 @@ 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/[email protected]/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, context } ) => {
const contextValue = useContext( context );
Object.keys( style )
.filter( ( n ) => n !== 'default' )
.forEach( ( key ) => {
const result = evaluate( style[ key ], {
key,
context: contextValue,
} );
element.props.style = element.props.style || {};
if ( typeof element.props.style === 'string' )
element.props.style = cssStringToObject(
element.props.style
);
if ( ! result ) delete element.props.style[ key ];
else element.props.style[ key ] = result;

useEffect( () => {
// This seems necessary because Preact doesn't change the styles on
// the hydration, so we have to do it manually. It doesn't need deps
// because it only needs to do it the first time.
if ( ! result ) {
element.ref.current.style.removeProperty( key );
} else {
element.ref.current.style[ key ] = result;
}
}, [] );
} );
}
);

// data-wp-bind--[attribute]
directive(
'bind',
Expand Down
118 changes: 118 additions & 0 deletions test/e2e/specs/interactivity/directives-style.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* Internal dependencies
*/
import { test, expect } from './fixtures';

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

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

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

test( 'dont change style if callback returns same value on hydration', async ( {
page,
} ) => {
const el = page.getByTestId(
'dont change style if callback returns same value on hydration'
);
await expect( el ).toHaveAttribute(
'style',
'color: red; background: green;'
);
} );

test( 'remove style if callback returns falsy value on hydration', async ( {
page,
} ) => {
const el = page.getByTestId(
'remove style if callback returns falsy value on hydration'
);
await expect( el ).toHaveAttribute( 'style', 'background: green;' );
} );

test( 'change style if callback returns a new value on hydration', async ( {
page,
} ) => {
const el = page.getByTestId(
'change style if callback returns a new value on hydration'
);
await expect( el ).toHaveAttribute(
'style',
'color: red; background: green;'
);
} );

test( 'handles multiple styles and callbacks on hydration', async ( {
page,
} ) => {
const el = page.getByTestId(
'handles multiple styles and callbacks on hydration'
);
await expect( el ).toHaveAttribute(
'style',
'background: red; border: 2px solid yellow;'
);
} );

test( 'can add style when style attribute is missing on hydration', async ( {
page,
} ) => {
const el = page.getByTestId(
'can add style when style attribute is missing on hydration'
);
await expect( el ).toHaveAttribute( 'style', 'color: red;' );
} );

test( 'can toggle style', async ( { page } ) => {
const el = page.getByTestId( 'can toggle style' );
await expect( el ).toHaveAttribute( 'style', 'color: red;' );
await page.getByTestId( 'toggle color' ).click();
await expect( el ).toHaveAttribute( 'style', 'color: blue;' );
} );

test( 'can remove style', async ( { page } ) => {
const el = page.getByTestId( 'can remove style' );
await expect( el ).toHaveAttribute( 'style', 'color: red;' );
await page.getByTestId( 'switch color to false' ).click();
await expect( el ).toHaveAttribute( 'style', '' );
} );

test( 'can toggle style in the middle', async ( { page } ) => {
const el = page.getByTestId( 'can toggle style in the middle' );
await expect( el ).toHaveAttribute(
'style',
'color: blue; background: red; border: 1px solid black;'
);
await page.getByTestId( 'toggle color' ).click();
await expect( el ).toHaveAttribute(
'style',
'color: blue; background: blue; border: 1px solid black;'
);
} );

test( 'handles styles names with hyphens', async ( { page } ) => {
const el = page.getByTestId( 'handles styles names with hyphens' );
await expect( el ).toHaveAttribute( 'style', 'background-color: red;' );
await page.getByTestId( 'toggle color' ).click();
await expect( el ).toHaveAttribute(
'style',
'background-color: blue;'
);
} );

test( 'can use context values', async ( { page } ) => {
const el = page.getByTestId( 'can use context values' );
await expect( el ).toHaveAttribute( 'style', 'color: blue;' );
await page.getByTestId( 'toggle context' ).click();
await expect( el ).toHaveAttribute( 'style', 'color: red;' );
} );
} );