Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
*/
?>

<div data-wp-interactive='{"namespace": "directive-class"}'>
<div
data-wp-interactive='{"namespace": "directive-class"}'
data-wp-context='{ "value": false }'
>
<button
data-wp-on--click="actions.toggleTrueValue"
data-testid="toggle trueValue"
Expand All @@ -21,6 +24,13 @@
Toggle falseValue
</button>

<button
data-wp-on--click="actions.toggleContextValue"
data-testid="toggle context value"
>
Toggle context value
</button>

<div
class="foo bar"
data-wp-class--foo="state.falseValue"
Expand Down Expand Up @@ -59,19 +69,11 @@ class="foo bar baz"
data-testid="can toggle class when class attribute is missing"
></div>

<div data-wp-context='{ "falseValue": false }'>
<div
class="foo"
data-wp-class--foo="context.falseValue"
data-testid="can use context values"
></div>
<button
data-wp-on--click="actions.toggleContextFalseValue"
data-testid="toggle context false value"
>
Toggle context falseValue
</button>
</div>
<div
class="foo"
data-wp-class--foo="context.value"
data-testid="can use context values"
></div>

<div
data-wp-class--block__element--modifier="state.trueValue"
Expand All @@ -83,4 +85,8 @@ class="foo"
data-testid="can use classes with several dashes"
></div>

<div
data-wp-class--#[^+-]$="context.value"
data-testid="class name no-aplhanumeric"
></div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ const { state } = store( 'directive-class', {
toggleFalseValue: () => {
state.falseValue = ! state.falseValue;
},
toggleContextFalseValue: () => {
toggleContextValue: () => {
const context = getContext();
context.falseValue = ! context.falseValue;
context.value = ! context.value;
},
},
} );
29 changes: 17 additions & 12 deletions packages/interactivity/src/directives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -354,19 +354,24 @@ export default () => {
.forEach( ( entry ) => {
const className = entry.suffix;
const result = evaluate( entry );
const currentClass = element.props.class || '';
const classFinder = new RegExp(
`(^|\\s)${ className }(\\s|$)`,
'g'
);
Comment on lines -365 to -368
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent of the other changes, this is likely a good change. Building a regex effectively from an input string like was done here is risky.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is necessary here because there are number of special characters that would need special processing in the Regexp like [].


const classProxyElement = document.createElement( 'div' );
if ( 'string' === typeof element.props.class ) {
classProxyElement.className = element.props.class;
}

if ( ! result ) {
element.props.class = currentClass
.replace( classFinder, ' ' )
.trim();
} else if ( ! classFinder.test( currentClass ) ) {
element.props.class = currentClass
? `${ currentClass } ${ className }`
: className;
if (
classProxyElement.classList.contains( className )
) {
classProxyElement.classList.remove( className );
element.props.class = classProxyElement.className;
}
} else if (
! classProxyElement.classList.contains( className )
) {
classProxyElement.classList.add( className );
element.props.class = classProxyElement.className;
}

useInit( () => {
Expand Down
22 changes: 14 additions & 8 deletions packages/interactivity/src/vdom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@ const isObject = ( item: unknown ): item is Record< string, unknown > =>

// Regular expression for directive parsing.
const directiveParser = new RegExp(
`^data-${ p }-` + // ${p} must be a prefix string, like 'wp'.
`^${ fullPrefix }` + // ${fullPrefix} is the expected prefix string: "data-wp-".
// Match alphanumeric characters including hyphen-separated
// segments. It excludes underscore intentionally to prevent confusion.
// E.g., "custom-directive".
'([a-z0-9]+(?:-[a-z0-9]+)*)' +
'(?:[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)' +
// (Optional) Match '--' followed by any alphanumeric charachters. It
// excludes underscore intentionally to prevent confusion, but it can
// contain multiple hyphens. E.g., "--custom-prefix--with-more-info".
'(?:--([a-z0-9_-]+))?$',
'i' // Case insensitive.
'(?:--)?'
);

// Regular expression for reference parsing. It can contain a namespace before
Expand Down Expand Up @@ -141,13 +140,20 @@ export function toVdom( root: Node ): Array< ComponentChild > {
if ( directives.length ) {
props.__directives = directives.reduce(
( obj, [ name, ns, value ] ) => {
const directiveMatch = directiveParser.exec( name );
if ( directiveMatch === null ) {
if ( ! directiveParser.test( name ) ) {
warn( `Found malformed directive name: ${ name }.` );
return obj;
}
const prefix = directiveMatch[ 1 ] || '';
const suffix = directiveMatch[ 2 ] || 'default';

const unPrefixed = name.slice( fullPrefix.length );
const splitIndex = unPrefixed.indexOf( '--' );
const [ prefix, suffix = 'default' ] =
splitIndex === -1
? [ unPrefixed ]
: [
unPrefixed.slice( 0, splitIndex ),
unPrefixed.slice( splitIndex + 2 ),
];

obj[ prefix ] = obj[ prefix ] || [];
obj[ prefix ].push( {
Expand Down
17 changes: 15 additions & 2 deletions test/e2e/specs/interactivity/directive-class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ test.describe( 'data-wp-class', () => {
test( 'can use context values', async ( { page } ) => {
const el = page.getByTestId( 'can use context values' );
await expect( el ).toHaveClass( '' );
await page.getByTestId( 'toggle context false value' ).click();
await page.getByTestId( 'toggle context value' ).click();
await expect( el ).toHaveClass( 'foo' );
await page.getByTestId( 'toggle context false value' ).click();
await page.getByTestId( 'toggle context value' ).click();
await expect( el ).toHaveClass( '' );
} );

Expand All @@ -108,4 +108,17 @@ test.describe( 'data-wp-class', () => {
const el = page.getByTestId( 'can use classes with several dashes' );
await expect( el ).toHaveClass( 'main-bg----color' );
} );

test( 'can use class names with non-alphanumeric characters', async ( {
page,
} ) => {
const expectedClassName = '#[^+-]$';
const el = page.getByTestId( 'class name no-aplhanumeric' );
const toggle = page.getByTestId( 'toggle context value' );
await expect( el ).not.toHaveClass( expectedClassName );
await toggle.click();
await expect( el ).toHaveClass( expectedClassName );
await toggle.click();
await expect( el ).not.toHaveClass( expectedClassName );
} );
} );