Skip to content

Commit 3c45243

Browse files
committed
Interactivity API: Fix popover bind hydration
1 parent 8ece9a1 commit 3c45243

5 files changed

Lines changed: 110 additions & 26 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/e2e-tests/plugins/interactive-blocks/directive-bind/render.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,35 @@
9898
</div>
9999
<?php endforeach; ?>
100100

101+
<?php
102+
$popover_cases = array(
103+
'true' => '{ "value": true }',
104+
'false' => '{ "value": false }',
105+
'null' => '{ "value": null }',
106+
'undef' => '{ "__any": "any" }',
107+
'auto' => '{ "value": "auto" }',
108+
'manual' => '{ "value": "manual" }',
109+
'hint' => '{ "value": "hint" }',
110+
);
111+
?>
112+
113+
<?php foreach ( $popover_cases as $type => $context ) : ?>
114+
<div
115+
data-testid='hydrating popover <?php echo $type; ?>'
116+
data-wp-context='<?php echo $context; ?>'
117+
>
118+
<div
119+
data-testid="popover"
120+
data-wp-bind--popover="context.value"
121+
></div>
122+
<button
123+
data-testid="toggle value"
124+
data-wp-on--click="actions.toggleValue"
125+
data-wp-bind--data-toggle-count="context.count"
126+
>Toggle</button>
127+
</div>
128+
<?php endforeach; ?>
129+
101130
<div data-wp-context='{"test": true}'>
102131
<div
103132
data-testid="without-unique-id"

packages/interactivity/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"types": "build-types",
3636
"dependencies": {
3737
"@preact/signals": "^1.3.0",
38-
"preact": "^10.24.2"
38+
"preact": "10.29.1"
3939
},
4040
"devDependencies": {
4141
"@types/jest": "^29.5.14"

packages/interactivity/src/directives.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ export default () => {
745745
/*
746746
* We set the value directly to the corresponding HTMLElement instance
747747
* property excluding the following special cases. We follow Preact's
748-
* logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L110-L129
748+
* logic: https://github.com/preactjs/preact/blob/10.29.1/src/diff/props.js#L115-L129
749749
*/
750750
if ( attribute === 'style' ) {
751751
if ( typeof result === 'string' ) {
@@ -774,6 +774,7 @@ export default () => {
774774
attribute !== 'rowSpan' &&
775775
attribute !== 'colSpan' &&
776776
attribute !== 'role' &&
777+
attribute !== 'popover' &&
777778
attribute in el
778779
) {
779780
try {
@@ -788,14 +789,21 @@ export default () => {
788789
* aria- and data- attributes have no boolean representation.
789790
* A `false` value is different from the attribute not being
790791
* present, so we can't remove it.
791-
* We follow Preact's logic: https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136
792+
* We follow Preact's logic: https://github.com/preactjs/preact/blob/10.29.1/src/diff/props.js#L138-L150
792793
*/
794+
if ( typeof result === 'function' ) {
795+
return;
796+
}
797+
793798
if (
794799
result !== null &&
795800
result !== undefined &&
796801
( result !== false || attribute[ 4 ] === '-' )
797802
) {
798-
el.setAttribute( attribute, result );
803+
el.setAttribute(
804+
attribute,
805+
attribute === 'popover' && result === true ? '' : result
806+
);
799807
} else {
800808
el.removeAttribute( attribute );
801809
}

test/e2e/specs/interactivity/directive-bind.spec.ts

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ test.describe( 'data-wp-bind', () => {
127127
* contain after hydration.
128128
*/
129129
entityPropValue: any,
130+
/**
131+
* Value that the attribute should contain after Preact
132+
* renders the same value. If omitted, the hydration value
133+
* is used.
134+
*/
135+
renderedAttributeValue?: any,
136+
/**
137+
* Value that the HTMLElement instance property should
138+
* contain after Preact renders the same value. If omitted,
139+
* the hydration value is used.
140+
*/
141+
renderedEntityPropValue?: any,
130142
]
131143
>;
132144
};
@@ -164,8 +176,8 @@ test.describe( 'data-wp-bind', () => {
164176
values: {
165177
false: [ null, 'false' ],
166178
true: [ null, 'true' ],
167-
null: [ null, '' ],
168-
undef: [ null, '' ],
179+
null: [ null, '', null, 'tacocat' ],
180+
undef: [ null, '', null, 'tacocat' ],
169181
emptyString: [ null, '' ],
170182
anyString: [ null, 'any' ],
171183
number: [ null, '10' ],
@@ -204,7 +216,12 @@ test.describe( 'data-wp-bind', () => {
204216
page,
205217
} ) => {
206218
for ( const type in values ) {
207-
const [ attrValue, propValue ] = values[ type ];
219+
const [
220+
attrValue,
221+
propValue,
222+
renderedAttrValue = attrValue,
223+
renderedPropValue = propValue,
224+
] = values[ type ];
208225

209226
const container = page.getByTestId( `hydrating ${ type }` );
210227
const el = container.getByTestId( testid );
@@ -230,18 +247,6 @@ test.describe( 'data-wp-bind', () => {
230247
propValue,
231248
] );
232249

233-
// Only check the rendered value if the new value is not
234-
// `undefined` and the attribute is neither `value` nor
235-
// `disabled` because Preact doesn't update the attribute
236-
// for those cases.
237-
// See https://github.com/preactjs/preact/blob/099c38c6ef92055428afbc116d18a6b9e0c2ea2c/src/diff/index.js#L471-L494
238-
if (
239-
type === 'undef' &&
240-
( name === 'value' || name === 'undefined' )
241-
) {
242-
return;
243-
}
244-
245250
await toggle.click( { clickCount: 2 } );
246251

247252
// Ensure values have been updated after toggling.
@@ -250,23 +255,65 @@ test.describe( 'data-wp-bind', () => {
250255
'2'
251256
);
252257

253-
// Values should be the same as before.
254258
const renderedAttr = await el.getAttribute( name );
255259
const renderedProp = await el.evaluate(
256260
( node, propName ) => ( node as any )[ propName ],
257261
name
258262
);
259263
expect( [ type, renderedAttr ] ).toEqual( [
260264
type,
261-
attrValue,
265+
renderedAttrValue,
262266
] );
263267
expect( [ type, renderedProp ] ).toEqual( [
264268
type,
265-
propValue,
269+
renderedPropValue,
266270
] );
267271
}
268272
} );
269273
}
274+
275+
test( 'popover is correctly hydrated for different values', async ( {
276+
page,
277+
} ) => {
278+
const values: Record< string, string | null > = {
279+
true: '',
280+
false: null,
281+
null: null,
282+
undef: null,
283+
auto: 'auto',
284+
manual: 'manual',
285+
hint: 'hint',
286+
};
287+
288+
// Ensure hydration has happened.
289+
const checkbox = page.getByTestId(
290+
'add missing checked at hydration'
291+
);
292+
await expect( checkbox ).toBeChecked();
293+
294+
for ( const type in values ) {
295+
const attrValue = values[ type ];
296+
const container = page.getByTestId(
297+
`hydrating popover ${ type }`
298+
);
299+
const el = container.getByTestId( 'popover' );
300+
const toggle = container.getByTestId( 'toggle value' );
301+
302+
const hydratedAttr = await el.getAttribute( 'popover' );
303+
expect( [ type, hydratedAttr ] ).toEqual( [ type, attrValue ] );
304+
305+
await toggle.click( { clickCount: 2 } );
306+
307+
// Ensure values have been updated after toggling.
308+
await expect( toggle ).toHaveAttribute(
309+
'data-toggle-count',
310+
'2'
311+
);
312+
313+
const renderedAttr = await el.getAttribute( 'popover' );
314+
expect( [ type, renderedAttr ] ).toEqual( [ type, attrValue ] );
315+
}
316+
} );
270317
} );
271318

272319
test( 'should ignore unique ids', async ( { page } ) => {

0 commit comments

Comments
 (0)