Style states: Use symbols for property keys to avoid clashes#79210
Conversation
|
Size Change: +970 B (+0.01%) Total Size: 7.5 MB 📦 View Changed
|
|
Flaky tests detected in 998c47f. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27932793016
|
| "selectors": { | ||
| "states": { | ||
| "@current": ".wp-block-navigation .current-menu-item" | ||
| "-current": ".wp-block-navigation .current-menu-item" |
There was a problem hiding this comment.
I think we talked about not needing the prefix for this? but it's not a blocker for this PR, we could do it on a followup
There was a problem hiding this comment.
I personally think it might be good to always use a prefix for style states as a convention. Otherwise, there's a situation where most states use prefixes, one or two don't and it's a bit inconsistent.
There was a problem hiding this comment.
Will third parties be able to define custom states at any point? If they won't, then it's not a problem either way as long as custom states don't use the same prefix as viewport states. But I agree that if all the other states have a prefix we might as well give one to custom states too 😄
Tbh I'm not convinced our hypothetical arbitrary viewport values would be a problem either, we could always just prefix anything theme- or plugin-defined. But if folks want to try this approach, this is as good a way of doing it as any (and better than an extra level of nesting imo)
There was a problem hiding this comment.
I agree with @tellthemachines here, I never was very convinced that the custom naming was a real problem here. And I think we should support 3rd parties adding states at some point, it could open up a lot of options for custom blocks.
tellthemachines
left a comment
There was a problem hiding this comment.
This is testing well for me, except of course that any existing responsive styles cease to be recognised.
The code changes look pretty straightforward.
What do we still need here for this to get to ready status? Shall we include some temporary back compat?
This is correct |
I'm looking at the back compat, just working through the options to try to come up with something relatively clean and not too difficult to remove in the future. |
| #### Responsive styles | ||
|
|
||
| Block styles can be scoped to two named breakpoints: `mobile` and `tablet`. Any style property that is valid at the block or element level can be nested under one of these keys. | ||
| Block styles can be scoped to two named breakpoints: `@mobile` and `@tablet`. Any style property that is valid at the block or element level can be nested under one of these keys. |
There was a problem hiding this comment.
I did some drive-by smoke testing, it's working well and I like the syntax direction!
Especially @mobile / @tablet for responsive states... avoids style property clashes without changing the shape.
One thing that occurred to me for later in the cycle: as pseudo, responsive, and custom selector states grow, we'll probably want some small unifying doc for "style states" and their key prefixes.
They seem to be documented separately, so a convention exists but could be explicit.
fd5ebe7 to
58940f4
Compare
a40e232 to
8199f01
Compare
|
Thanks for running with this idea folks, I appreciate it :) |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
There should be back compat now, and from what I can tell it's working. 😄 |
andrewserong
left a comment
There was a problem hiding this comment.
Nice work on this @talldan! It's been testing well for me so far.
One question came up while running this through a Claude session: and that's if it's possible to only perform the normalization for global styles once on load rather than every time we call setStyle? I'm not sure if it's really an issue in practice, but currently it looks like the normalization in setStyle does a full tree traversal via normalizeStyleStateAliases and then normalizeStyleStateNode, so I'm wondering if it's possible to perform this once, rather than every time we set the style?
Yeah, that's a good call. I'll see if it's possible. |
|
@andrewserong I can't see a way that this is possible, at least not without some re-plumbing that might cause unknown side effects. The best candidate for migrating incoming user style values looks to be The part that looks problematic to me is that when setting global styles, the code gets them first from the core store: gutenberg/packages/editor/src/components/global-styles/hooks.js Lines 132 to 144 in 02cc069 That result from There are two options:
|
|
Thanks for digging in @talldan! I suspect it's a negligible issue given that a) the tree likely isn't going to be very big and b) the migration is guarded behind running just in the GB plugin. I think we could likely go with what you've got and revisit if it turns out to be a problem. |
andrewserong
left a comment
There was a problem hiding this comment.
Just giving this a tentative approval as I'm about to go AFK for a long weekend and wanted to signal that I think this a good approach and feels like it's in good shape to land.
I've left a comment about whether it's worth moving resolve_style_state_aliases to a utility function in a separate file if we know we're not going to backport it.
Other than that, I think the main thing to confirm is just that everyone's happy with the new prefixes. It seems so! As such my vote would be to get this PR in, and if there's any performance issues with the transformations, we could look into those in follow-ups.
In my manual testing this was all working nicely with post content created before this PR being handled correctly on the site frontend, and migrated in post content once I go to make changes to a post. I'm not sure I tested all the changes thoroughly, but I used the following markup for my smoke testing:
<!-- wp:group {"style":{"spacing":{"blockGap":"48px"},"mobile":{"spacing":{"blockGap":"4px"}}},"layout":{"type":"default"}} -->
<div class="wp-block-group">
<!-- wp:paragraph {"style":{"color":{"text":"#1e1e1e"},"mobile":{"color":{"text":"#cc0000"}},"tablet":{"color":{"text":"#0000cc"}}}} -->
<p class="has-text-color" style="color:#1e1e1e">Legacy responsive paragraph: red below 480px, blue 480–782px, default above. The gap between these two paragraphs also shrinks on mobile.</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Second paragraph (used to show the responsive blockGap).</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->LGTM!
| /** | ||
| * Resolves temporary aliases for persisted style state keys to their canonical keys. | ||
| * | ||
| * This keeps compatibility for Gutenberg plugin content created before | ||
| * responsive states used '@' and custom states used '-'. Remove after the | ||
| * deprecation window ends. | ||
| * | ||
| * DO NOT BACKPORT TO CORE. | ||
| * | ||
| * @param array $styles Persisted style data. | ||
| * @param string|null $block_name Current block name, when walking block styles. | ||
| * @return array Style data with canonical style state keys. | ||
| */ | ||
| public static function resolve_style_state_aliases( $styles, $block_name = null ) { |
There was a problem hiding this comment.
Not a blocker: but if we know we don't want to backport this to core, would it make sense to have this be a utility function that lives outside of this file, e.g. something like gutenberg_resolve_style_state_aliases? That way it's kept a bit separate from the overall Theme JSON class?
There was a problem hiding this comment.
Alternatively, in /lib/experimental or lib/compat/plugin or lib/block-supports/states.php ?
There was a problem hiding this comment.
lib/compat/plugin looks like the right option 👍
| if ( normalizedStyle !== newAttributes.style ) { | ||
| newAttributes.style = normalizedStyle; | ||
| } |
There was a problem hiding this comment.
This tripped me up for a second, I'm so used to functions that always return new objects but this only returns a new object if the original one had the legacy state, so the strict equality check with two objects is the right check. Again not a blocker, but is it worth adding a comment to explain this nuance? (No worries if it feels obvious to you or others!)
|
This is testing well for me as well. Great work. 💯 Tested with the following HTML for backwards compat. Block editor and frontend LGTM. Example block HTML<!-- wp:group {"style":{"mobile":{"border":{"color":"#00b7fa","width":"34px"}},"tablet":{"border":{"color":"#00661d","width":"7px"}},"border":{"color":"#bd0000","width":"84px"}},"layout":{"type":"constrained"}} -->
<div class="wp-block-group has-border-color" style="border-color:#bd0000;border-width:84px"><!-- wp:group {"style":{"color":{"background":"#e14141"},"tablet":{"color":{"background":"#0cdd08"},"typography":{"fontSize":"var:preset|font-size|x-large"}},"mobile":{"color":{"background":"#33c8e6"},"typography":{"fontSize":"var:preset|font-size|xx-large"}}},"fontSize":"large","layout":{"type":"constrained"}} -->
<div class="wp-block-group has-background has-large-font-size" style="background-color:#e14141"><!-- wp:paragraph {"style":{"color":{"text":"#ff0000"},"elements":{"link":{"color":{"text":"#ff0000"}}},"tablet":{"color":{"text":"#009936"},"elements":{"link":{"color":{"text":"#009936"}}}},"mobile":{"color":{"text":"#5266ff"},"elements":{"link":{"color":{"text":"#5266ff"}}}}}} -->
<p class="has-text-color has-link-color" style="color:#ff0000">Heading</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"style":{"elements":{"link":{"color":{"text":"var:preset|color|accent-1"},":hover":{"color":{"text":"#f50000"}}}},"tablet":{"elements":{"link":{"color":{"text":"#00ffd4"},":hover":{"color":{"text":"#079400"}}}}},"mobile":{"elements":{"link":{"color":{"text":"#00f7ff"},":hover":{"color":{"text":"#fffb19"}}}}}}} -->
<p class="has-link-color"><a href="https://playground.wordpress.net/scope:sad-vintage-garden/sample-page/" data-type="page" data-id="2">paragraph</a></p>
<!-- /wp:paragraph --></div>
<!-- /wp:group --></div>
<!-- /wp:group -->
<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph --> |
| }; | ||
| } | ||
|
|
||
| const normalizedStyle = normalizeStyleStateAliases( newAttributes.style ); |
There was a problem hiding this comment.
Worth gating behind newAttributes.style?
There was a problem hiding this comment.
Sorry, I mean something like if ( newAttributes.style ) { so it's only called when style is present.
There was a problem hiding this comment.
I should have checked: normalizeStyleStateAliases already does it 👍🏻
Refactor Update filename / add doc blocks Improve ts back compat code Fix Remove parser tests
8199f01 to
4fe5a67
Compare
|
I think I've fixed all the feedback, so I'll go ahead and merge. I've a core PR here implementing the same PHP changes - WordPress/wordpress-develop#12253. I'll see if I can also push a commit to WordPress/wordpress-develop#11174 thanks for all the reviews 🎉 |
What?
The way style state keys are plain keys in global styles / block attributes has been pointed out as a potential risk
An example of this is something like this global style state, where
mobileis the style state key:In the future it may be possible for users to define arbitrary responsive states, so the
mobilekey could be anything, potentially clashing with known keys that are used for other style properties.How?
Two potential solutions spring to mind to solve this. The first option could be to nest the properties in a
statesproperty. This is made difficult by pseudo states already being shipped in WordPress.The second is to use some kind of prefix for the state keys. This is actually already somewhat in place, pseudo states are always prefixed with a
:(e.g.:hover).@MaggieCabrera has also been looking at implementing custom states, currently there's an
@currentstate for styling the navigation block's current menu item in global styles.The
@prefix is actually perfect for responsive states, as it mirrors the css syntax. This PR proposes using the following prefixes::for pseudo states (e.g.:hover,:focus). This remains unchanged@for responsive states (e.g.@mobile,@tablet). This is newly prefixed.-for 'custom' states (e.g.-current). This was previously@, but is now-. These custom states are generally represented by classnames that are applied to a block. A.prefix is an option, but it might make paths to properties a bit unusual (e.g.@mobile..current.color.textvs@mobile.-current.color.text)Testing Instructions
This needs a smoke test of the various features to ensure they work the same as in trunk.
Use of AI Tools
Codex / Opencode