-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Blocks: Fix incorrect placement for hooked blocks in the parent container #54349
Conversation
This pull request has changed or added PHP files. Please confirm whether these changes need to be synced to WordPress Core, and therefore featured in the next release of WordPress. If so, it is recommended to create a new Trac ticket and submit a pull request to the WordPress Core Github repository soon after this pull request is merged. If you're unsure, you can always ask for help in the #core-editor channel in WordPress Slack. Thank you! ❤️ View changed files❔ phpunit/fixtures/hooked-block/block.json ❔ phpunit/tests/blocks/renderHookedBlocks.php ❔ lib/compat/wordpress-6.4/block-hooks.php |
0b93c5e
to
ff1c019
Compare
@ockham, should we add unit tests directly to WordPress core and only apply the fix to the logic in the Gutenberg plugin? Well, if we are going to keep |
I think it makes sense to keep unit tests here, at least for now 👍 (Definitely good for confidence in the bugfix.) |
lib/experimental/block-hooks.php
Outdated
$chunk_index = count( $block['innerContent'] ) - 1; | ||
for ( $index = $chunk_index; $index >= 0; $index-- ) { | ||
if ( is_null( $block['innerContent'][ $index ] ) ) { | ||
$chunk_index = $index; | ||
break; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noting that this sets $chunk_index
to -1
if $block['innerContent']
is empty. Which I think is okay, since array_splice()
also works if the offset arg is negative (see).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I think I should set $chunk_index
to 0 initially and only update when null
is in the array. I now realized that it's possible that the array is empty 😄
I was studying unit tests for the parser earlier today, some interesting findings for innerContent
:
gutenberg/packages/block-serialization-spec-parser/shared-tests.js
Lines 241 to 289 in 7652de9
describe( 'innerBlock placemarkers', () => { | |
test( 'innerContent exists', () => { | |
expect( parse( 'test' )[ 0 ] ).toHaveProperty( 'innerContent', [ | |
'test', | |
] ); | |
expect( parse( '<!-- wp:void /-->' )[ 0 ] ).toHaveProperty( | |
'innerContent', | |
[] | |
); | |
} ); | |
test( 'innerContent contains innerHTML', () => { | |
expect( | |
parse( '<!-- wp:block -->Inner<!-- /wp:block -->' )[ 0 ] | |
).toHaveProperty( 'innerContent', [ 'Inner' ] ); | |
} ); | |
test( 'block locations become null', () => { | |
expect( | |
parse( | |
'<!-- wp:block --><!-- wp:void /--><!-- /wp:block -->' | |
)[ 0 ] | |
).toHaveProperty( 'innerContent', [ null ] ); | |
} ); | |
test( 'HTML soup appears after blocks', () => { | |
expect( | |
parse( | |
'<!-- wp:block --><!-- wp:void /-->After<!-- /wp:block -->' | |
)[ 0 ] | |
).toHaveProperty( 'innerContent', [ null, 'After' ] ); | |
} ); | |
test( 'HTML soup appears before blocks', () => { | |
expect( | |
parse( | |
'<!-- wp:block -->Before<!-- wp:void /--><!-- /wp:block -->' | |
)[ 0 ] | |
).toHaveProperty( 'innerContent', [ 'Before', null ] ); | |
} ); | |
test( 'blocks follow each other', () => { | |
expect( | |
parse( | |
'<!-- wp:block --><!-- wp:void /--><!-- wp:void /--><!-- /wp:block -->' | |
)[ 0 ] | |
).toHaveProperty( 'innerContent', [ null, null ] ); | |
} ); | |
} ); |
In particular, the fact that inner block locations always become null
, so you can have multiple of them. However, if there are no inner blocks then we won't have null
, and it can even be an empty array 🤔
I need to study some existing blocks to see how they serialize when there are no inner blocks. Group, Columns, Column, it might get tricky. I'm wondering if at least for core blocks we always enforce a HTML wrapper (I would assume yes, but it isn't a requirement for the parser), and whether we show the wrapper when there are no inner blocks inserter. I hope that with Columns that can be explored.
Basically, what @ockham raised in #54349 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example for the Columns block with 3 columns and the middle one has no child blocks:
<!-- wp:columns -->
<div class="wp-block-columns"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:paragraph -->
<p>First</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:paragraph -->
<p>Third</p>
<!-- /wp:paragraph --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->
A Group block with no child blocks:
<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group"></div>
<!-- /wp:group -->
A Group Block with a Cover block where I removed inner blocks:
<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:cover {"url":"http://localhost:8888/wp-content/uploads/2023/09/Maldives.jpg","id":18,"dimRatio":50,"layout":{"type":"constrained"}} -->
<div class="wp-block-cover"><span aria-hidden="true" class="wp-block-cover__background has-background-dim"></span><img class="wp-block-cover__image-background wp-image-18" alt="" src="http://localhost:8888/wp-content/uploads/2023/09/Maldives.jpg" data-object-fit="cover"/><div class="wp-block-cover__inner-container"></div></div>
<!-- /wp:cover --></div>
<!-- /wp:group -->
I wonder if there are any blocks with inner blocks that don't include any block wrapper HTML. It seems unlikely; I guess we'll find out (and can adjust the code accordingly if needed) 😅 In theory, a hooked block can be inserted as a first or last child into a block that doesn't have any inner blocks at all. This probably doesn't make any sense (and maybe we should forbid it; though that might not be straight-forward); in any case, I think our logic would work even then (and at least not crash 😬 ). Furthermore, I wonder if there's ever going to be more than one consecutive non-null array element in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One documentation nitpick; plus I agree that
It would be great to land #54293 first to remove the need for special code for bootstrapping PHP unit tests.
Other than that:
634c0a6
to
013f74c
Compare
Co-authored-by: Bernie Reiter <96308+ockham@users.noreply.github.com>
Props to @ockham for spotting issues.
013f74c
to
b737b99
Compare
As documented in #54349 (comment), while the block parser allows inner blocks without a wrapper, in practice it doesn't happen because the way inner blocks are implemented in the block editor enforces it with the current design. The block wrapper is necessary for the UI to work correctly, so while it's still possible to not include it, it would most likely make it impossible to select the block without using the List View.
I added unit tests that document this case when there is also a block wrapper. The way it currently works isn't perfect, but it definitely won't crash. We can discuss later what to do about it.
It's possible, see the example for the Columns block in the block fixtures folder from integration tests: gutenberg/test/integration/fixtures/blocks/core__columns.parsed.json Lines 32 to 38 in 999b49c
|
Sorry, I don't see it 😅 In your example, non-null and "innerContent": [
"\n\t<div class=\"wp-block-column\">\n\t\t",
"\n\t\t",
null,
"\n\t</div>\n\t"
] |
Interesting observation. I wasn't aware it groups HTML into one entry before and one after the child block. Anyway, the loop will run twice in most of the cases when there is a block wrapper, so it's probably fine. |
What?
Fixes #54307.
Every block that has inner blocks also has some wrapping HTML element. The way hooked blocks are currently implemented is that it puts the injected block at exactly first (
firstChild
) or last place (lastChild
) in the parsed block, which happens to be outside the wrapping element. A simplified example based on REST API response that shows the issue:Why?
The injected block is misplaced and therefore incorrectly styled on the front end. It should be with the
main
tag instead in the example provided:How?
Instead of injecting the hooked block as the first item (
firstChild
) or the last item (lastChild
), there is no logic that tries to locate the exact place where the child block is marked withnull
in the array containinginnerContent
of the block. Once the firstnull
value is found, then anothernull
value is placed next to it. In the case, when there is no child block (nonull
value), then thenull
marker is added as the first item (firstChild
) or the last item (lastItem
) in the array.Note: the handling for the case with no inner blocks but the existing HTML wrapper for inner blocks isn't handled perfectly. We can discuss options for improving that separately.
Testing Instructions
core/comment-template
block.There is now a test coverage presenting how hooked blocks get injected depending on the state of the container block:
Screenshots or screencast