Open dabowman opened 2 years ago
@dabowman Your idea would be a wonderful advancement to the block editor, especially now that we're trying to use it for more than just post_content
.
... pass a second set of settings for a block that only applies to a specified context would probably do what we need. Doing this in theme.json would be nice.
(Forgive me if I'm bolting something alien on to your main point.) In addition to context-specific settings, I would like more ability to set context-specific allowedBlocks
. The current state of the block editor is a bit of a sledgehammer, allowing all blocks everywhere with one set of settings applied site-wide.
It would be much more useful for my client teams to be able to, e.g., have certain PHP page templates, custom post types, or templateParts
only allow a subset of blocks with an adjusted set of block settings. A realistic example is an editable header message area that only accepts core/heading
(with only h2
) and core/paragraph
with no inline formatting at all.
The ideal system would be one where a long list of site-wide default block settings are defined, and then "a-la-carte" subsets/adjustments of those blocks and settings are applied to certain editable areas of the site.
My only specific suggestion to your example is to make the block slugs more explicit to smoothly allow multiple block settings under one parent. Like this:
my-custom-blocks/page-header {
"core/heading": {
"typography": {
"fontSizes": [
{
"slug": "heading-03",
"size": "var(--wp--custom--fs--5)",
"name": "Heading 3"
},
{
"slug": "heading-04",
"size": "var(--wp--custom--fs--6)",
"name": "Heading 4"
}
]
}
},
"core/paragraph": {
adjusted settings...
},
"core/image": {
adjusted settings...
},
}
Thanks for sharing; I've thought about this as well and I think there are some existing issues out there that discuss this - https://github.com/WordPress/gutenberg/issues/35114 comes to mind but I'd imagine that there are others; you may be interested in also following discussion at https://github.com/WordPress/gutenberg/discussions/41349
Hey @dabowman, thanks for chiming in and sharing the detailed use case. The first point — contextually control what tools and presets blocks get access to — is currently captured in the roadmap in two issues:
The idea is that theme.json objects are composable, and that any post type, template, or template part can become a provider. That means you could have something akin to a theme.header.json
that would be used on a header template part. But the mechanisms are inherently flexible that could make any Group or pattern container a provider. See:
Those issues need some help, including fleshing out the ergonomics — should we default to separate files from the get go? Should the files be stored in /styles
or collocated with templates and template parts?
Ensuring theme.json objects merge well and in the right order is also paramount for things like #41707.
The second point about permissions is a bit trickier in how it can multiply the declarations. It's not clear if granular control over each setting type needs its own capability management or if it can be a wider set of principles (for example, control access to appearance tools in general by role). This needs a lot more thought to balance flexibility and complexity.
There's an issue for template permissions that is relevant to take into account: https://github.com/WordPress/gutenberg/issues/27597
@dabowman, may I ask you how far adding descendant block styles to your container blocks could get you? What would be still missing from what you are trying to accomplish?
@luisherranz Thank you for sharing! I'm working with @dabowman on some related work and took a look at your changes. I think the main difference is that we're looking to define a set of acceptable options on a hierarchy rather than a specific choice. Using the last example in the description, we'd want core/heading
to have a handful of correct font sizes that fit in with the design. When a heading is in a page-header
block, this would be limited to only 3 correct sizes that fit the design.
As far as I'm aware block styles can only dictate one "correct" style, and this would still be the case in a hierarchy with your changes. A core/heading
could be set as size X, and a page-header
containing a core/heading
could be size Y. This would be limiting, as we're trying to represent a set of allowed styles contextually rather than a single defined correct style. Is that right? I think using block settings would allow us to represent a set of allowed styles in UI where style-level settings wouldn't.
Oh, you're right, sorry. This one includes the settings: https://github.com/WordPress/gutenberg/pull/42124
@luisherranz Thank you, didn't realize that work was being done as well! I'll look more into it tomorrow. Does it support multi-level nesting, something like this?
{
"settings": {
"blocks": {
"core/heading": { /* top-level heading settings */},
"custom/page-header": {
"blocks": {
"core/heading": { /* location-specific heading settings */}
}
}
}
}
}
This is not an enhancement of the theme.json
. This lets you override the children's styles/settings of a container block.
<!-- wp:custom/page-header { "settings": { "blocks": { "core/heading": location-specific heading settings } } } -->
<!-- wp:heading -->
<h2>The custom/page-header block overrides this block's settings.</h2>
<!-- /wp:heading -->
<!-- /wp:custom/page-header -->
@luisherranz Just to clarify, I meant putting the above nested rules into a container. To change the example a bit, image we'd like different options for vanilla headings and those in core/media-text
blocks:
<!-- wp:group {"settings": {"blocks": {"core/heading": { /* top-level settings... */},"core/media-text": {"blocks": {"core/heading": { /* location-specific settings */}}}}}} -->
<!-- wp:heading -->
<h2>A normal heading with top-level settings</h2>
<!-- /wp:heading -->
<!-- wp:media-text {"mediaType":"image", /* ... */} -->
<div class="wp-block-media-text alignwide is-stacked-on-mobile"><figure class="wp-block-media-text__media"><img src="/*...*/" alt="" class="wp-image-22 size-full"/></figure><div class="wp-block-media-text__content">
<!-- wp:heading -->
<h2>Inner Heading</h2>
<!-- /wp:heading -->
<!-- /wp:media-text -->
<!-- /wp:group -->
I’m trying to figure out if it’s a valid strategy to put all of the design system logic into one container block at the top of the page that could specify all of the nested settings logic we’re looking for here using your new feature. I believe we’d need that sort of settings hierarchy to make the system work. If not, can you explain a bit more about what you had in mind? Thank you!
That's a good example, thanks.
Top-level settings are still defined in the theme.json
(and global styles) settings. You don't need to add settings to a top-level block for that unless you want to override them.
If the header settings need to be different inside a specific core/media-text
block, then you need to add those settings to the container:
<!-- wp:group -->
<!-- wp:heading -->
<h2>A normal heading with top-level settings from `theme.json`</h2>
<!-- /wp:heading -->
<!-- wp:media-text {"mediaType":"image", "settings": { "blocks": { "core/heading": { /* localtion-specific settings /* } } } } -->
<div class="wp-block-media-text alignwide is-stacked-on-mobile">...
<!-- wp:heading -->
<h2>Location specific settings from parent `core/media-text`</h2>
<!-- /wp:heading -->
</div>
<!-- /wp:media-text -->
<!-- /wp:group -->
From your comment, I guess you want to configure the settings of any heading block inside any media-text block, not only the headings inside a specific media-text instance. Is that correct?
From your comment, I guess you want to configure the settings of any heading block inside any media-text block, not only the headings inside a specific media-text instance. Is that correct?
That's correct! We are looking for a place (in theme.json
hopefully, but exploring other options) to define what an end-user of the block editor sees when creating a block in Gutenberg. We want to enforce a design system for all blocks such that a non-designer user only sees curated choices that fit in with the design system based on context. There are a couple of contexts @dabowman covers in this issue description, block context and user context:
The trouble is that this only happens on a theme-wide level. We want to be able to define all possible options that a certain block supports and then narrow down what options are presented to a user in the block editor at any given time based on a few context-based factors:
Where is the block? what is its’ parent? what template is it in? what post type is it on? what block is immediately before and after it? etc.
These questions are ones we consider when designing and the answers to them can impact what options we make available to a user.
Who is interacting with it? what permissions does the user interacting with it have? are they a designer? are they just a copy editor?
The nesting-based context in this issue would be very useful, but we're also looking into more generalized ways we can tie post type/current user/surrounding context into the block editor. The overall goal is a way that we can define block editor UI choices for enterprise customers who want a coherent design system enforced over one or more sites.
Thanks, Alec.
we're also looking into more generalized ways we can tie post type/current user/surrounding context into the block editor
Post type context
It would be easy to achieve with descendant block styles/settings just by creating a new template for each post type, adding a Group block at the top, and configuring the theme-json
overrides there.
Templates themselves could also do theme-json
overrides, which would be even more straightforward for this use case.
Current user context
I guess this is something that would need to be baked into the theme.json
system somehow. As Matías said, it can multiply the declarations, so if this is ever added, it should probably allow cascade configurations to minimize the problem.
Also, if this is ever considered, I'm not sure how far Gutenberg should go enforcing those permissions in the backend because the WordPress permission system doesn't support such granularity, AFAIK.
Surrounding context
@luisherranz Thank you for the breakdown about how we might approach different types of enterprise governance needs above.
We've been discussing implementation of these block editor settings. It seems like we have two main options:
theme.json
to handle nesting, role-based permissions settings, and possibly custom block settings to account for each use-case.I'd like to discuss the second option for a bit. This is actually sort of possible to do already with the useSettings
hook to provide context-aware block settings in code.
Here's a example of a nesting hierarchy where we alter core/heading
palette options whenever nested in a core/media-text
:
function setupGovernance() {
// When a core/media-text contains a core/heading, change to a limited palette
addNestedBlockSettings( 'core/media-text', 'core/heading', () => ( {
color: {
palette: {
theme: getThemeSettings( 'colors', [ 'primary', 'secondary' ]),
},
},
} ) );
}
const addNestedBlockSettings = ( parentBlockName, childBlockName, settingsCallback ) => {
// Enable __experimentalSettings on block to allow for per-block settings on this block type
const enableBlockExperimentalSettings = ( settings, name ) => {
if ( name === childBlockName ) {
settings.supports.__experimentalSettings = true;
}
return settings;
}
wp.hooks.addFilter('blocks.registerBlockType', 'plugin/enable-block-experimental-settings', enableBlockExperimentalSettings);
const withCustomNestingSettings = createHigherOrderComponent( WrappedBlock => {
return ( props ) => {
const isNestedInParentBlockName = useSelect( ( select ) => {
const { getBlockParents, getBlocksByClientId } = select( 'core/block-editor' );
const parentIds = getBlockParents( props.clientId, [ parentBlockName ] );
const parents = getBlocksByClientId( parentIds );
return parents.some( block => parentBlockName === block.name );
} );
useEffect( () => {
if ( props.name === childBlockName && isNestedInParentBlockName ) {
props.setAttributes( {
settings: settingsCallback(),
} );
}
}, [] );
return <WrappedBlock { ...props } />;
};
}, 'withCustomNestingSettings' );
defaultHooks.addFilter('editor.BlockEdit', 'plugin/custom-nesting-settings', withCustomNestingSettings);
};
function getThemeSettings( path, slugs = [] ) {
const settings = select( blockEditorStore ).getSettings();
let settingsAtPath = lodash.get(settings, path, []);
if ( slugs ) {
settingsAtPath = settingsAtPath.filter( setting => slugs.includes(setting.slug) );
}
return settingsAtPath;
}
setupGovernance();
Here's what that code looks like in action:
I also don't think this was the intended use of useSettings
, but the new settings hierarchy does allow for some cool possibilities. This code is a bit hacky and doesn't handle edge-cases well but it does demonstrate what code-based block governance might look like. Most of our current governance needs could be solved by applying context-specific settings to blocks like this.
Expressing governance declaratively in code as shown above would be powerful. If extended to a more officially supported use-case, WordPress developer users could define their own rules in a plugin using well-defined hooks. The obvious benefit is we wouldn't need to encode all types of governance in the schema and we'd still give enterprisey users the power to implement their own governance.
The idea of adding hooks to support our use-case in code seems very common in PHP WordPress, but to a lesser degree in Gutenberg. From my very limited perspective it seems Gutenberg aims to provide full configuration through theme.json
over extending behavior into plugins. Is that true, and is there a foundational reason for it?
If it were possible to add/extend existing hooks in Gutenberg to support our desired block editor governance through plugin code, would this be a welcome addition?
@ingeniumed and I are looking to help however we can to implement governance with Gutenberg. We want to solve @dabowman's needs, and enterprise use of design systems with Gutenberg more broadly. Nested block governance would be a great starting point. Whether this is by jumping into existing issues and PRs to extend theme.json, or implementing hooks to extend the API in code, we'd like to contribute.
We've read through the issues related subissues linked in this discussion but it's difficult to understand a concrete place where we can start pitching in. It might make sense to start a PR to implement nested block settings, work on a lower-level issue like theme.json cascading merge rules, or extend hooks like discussed above. Do you have any recommendations about where we can begin to contribute?
Do you have any recommendations about where we can begin to contribute?
I'm sorry, but I'm not familiar with that part of the codebase. From what I've seen so far, the best way to bring attention to a topic is to open a PR and ask people to review it. It doesn't have to be the full feature, just a meaningful part or first step. If someone has concerns with that approach, they should say so before merging it, ping more people for opinions, and so on.
I can only say that your proposal of nested block settings makes sense to me from a theme DX point of view. I can't imagine a different future need for that nested declaration that would cause a conflict with it and the mental model looks straightforward to me.
The only thing I'd pay extra attention to is if it has implications for the cascading algorithm. So I'd also follow the work of @mcsf, @jorgefilipecosta and @oandregal with the descendant styles/settings.
Thanks for all the feedback everyone!
@alecgeatches and I worked on this issue, and we have a PR up to address this here. We have linked back to this issue within that PR, and also included a brief demo video of it in action.
We had a call with @alecgeatches to better understand the problem and what kind of solutions could come from Core. Below is a summary.
settings
attribute — #40318).allowedBlocks
) can act as guardrails, while different block variations or patterns would appear at different times in order to set up contexts with their own settings
attributes.addNestedBlockSettings
would be a big departure from that.Instead, it seems sensible to consider adding some escape hatches in the form of filters. For admins, as long as their "contract" with end users so allows, this unblocks cascades of governance rules as complex as they need to be.
I quickly tried adding a filter to useSetting
. In the end this is how I could, as a third party, recreate the rule that narrows down the palette of any Headings contained somewhere inside a Group:
const RESTRICTED_PALETTE = [
{ color: '#1a4548', name: 'Primary', slug: 'primary' },
];
addFilter(
'blockEditor.useSetting.before',
'mcsf/limit-heading-palette-inside-groups',
( blockName, path, ancestors ) => {
if (
path === 'color.palette.theme' &&
blockName === 'core/heading' &&
ancestors.length > 1
) {
const { getBlockName } = wp.data.select( 'core/block-editor' );
if (
ancestors.some(
( ancestorId ) =>
getBlockName( ancestorId ) === 'core/group'
)
) {
return RESTRICTED_PALETTE;
}
}
}
);
For completion's sake, here's the diff in the source of useSetting
:
diff --git a/packages/block-editor/src/components/use-setting/index.js b/packages/block-editor/src/components/use-setting/index.js
index 3d37a85de1..c4d7f99594 100644
--- a/packages/block-editor/src/components/use-setting/index.js
+++ b/packages/block-editor/src/components/use-setting/index.js
@@ -11,6 +11,7 @@ import {
__EXPERIMENTAL_PATHS_WITH_MERGE as PATHS_WITH_MERGE,
hasBlockSupport,
} from '@wordpress/blocks';
+import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies
@@ -127,6 +128,15 @@ export default function useSetting( path ) {
...select( blockEditorStore ).getBlockParents( clientId ),
clientId, // The current block is added last, so it overwrites any ancestor.
];
+
+ result = applyFilters(
+ 'blockEditor.useSetting.before',
+ blockName,
+ normalizedPath,
+ candidates
+ );
+
+ if ( result !== undefined ) {
candidates.forEach( ( candidateClientId ) => {
const candidateBlockName =
select( blockEditorStore ).getBlockName(
I think this is worth looking into. We might want to introduce two filters: one suffixed .before
and one .after
; the former short-circuits useSetting
, the latter intercepts the computed value. Special care should be put into performance — some quick debugging shows that useSetting
fires very often, which is something to address regardless of the filters.
@mcsf Thank you for the summary and feedback! We're going to spend some time trying to reimplement the system via a plugin as you've suggested. This is very similar to a previous implementation we suggested with the bonus of a much cleaner hook.
One initial problem we've hit is that without the involvement of get_settings_nodes()
generating selectors from nested settings, nested classes like palette color selections only work if also defined on the root element in theme.json
. Nested settings that only are applied on children will have no applicable CSS generated. We'll see if we can get around this with custom CSS generation similar to Gutenberg's approach, or maybe there's an opportunity for another hook to avoid that.
I have one question about this point above (emphasis mine):
- However, it should be reminded that theme.json configurations are just one half of Global Styles and Settings, the other half being the UIs (the global sidebar, block controls, etc.). Configurations should remain easily visually representable — while we want GSS to be flexible, it should retain a sense of simplicity, because the system is as much about theme authors as about end users. An API like the proposed
addNestedBlockSettings
would be a big departure from that.
Your team mentioned this in the call as well, and I'm not sure what this refers to. Can you tell me a little more about what you mean by visually representable? In the case of the nested PR, the rules are represented in theme.json
are visually represented in the block controls. Is there another UI representation of theme.json
/block.json
/per-section settings and styling rules beyond the block editor itself?
Thank you!
@mcsf Building on what @alecgeatches mentioned, there's an approach we have in mind but it requires some confirmation.
Essentially, we would want to have a filter that's applied at the end of the sanitize function so that anyone can modify the output of the processed theme.json. This is the specific line that I am referring to. Instead of the regular logic wherein the theme.json is cleaned up based on the schema, there would be the chance for any plugin to add/remove/update the data inside.
The purpose for this would be to add in support for unsupported theme.json properties such as nested block settings. They would now be able to be available to the editor, courtesy of any function hooking into this filter.
The idea for this came from this existing filter that's used by Gutenberg already to modify the output of WP_Theme_JSON's function.
I see theme_json_get_style_nodes
as a problematic filter we should aim to deprecate, if we can. It hooks into a specific implementation, preventing us from making any modifications (for example, remove that class entirely in favor of style engine code, etc). Styles and its flows are a very dynamic area, as the changes to the style engine, theme.json code, and block.json testify. I wouldn't want to add filters that only modify implementation details of our current code.
If the goal is supporting new things that the core engine doesn't, would it work for you to:
block_editor_setting
hook (don't remember the exact name).@oandregal Thanks for the feedback, and for explaining that a bit more! incidentally, the route you are suggesting is what we decided to take after my previous comment. We ended up not going down the filters route. Instead, what we did is:
blockEditor.useSetting.before
as a hook to useSetting
on the JS side to allow us to inject block settings on to the editor. This would be the settings point you noted.get_blocks_metadata
function on the PHP side to get access to things like valid_block_names, selectors, etc. This ties into the settings point, and also the styles point.This way we are able to minimize what we change in the current code, and like you said try to not add in filters that only modify implementation details of our current code. It would be more for enhancing it.
The part that I'm working on, that I could use your advise on is:
How to get the additional block settings injected as CSS attributes onto the editor? At the moment, we are able to select the setting (change the colour to red for example) from the editor's block panel. But, since the CSS attribute has not been injected into the styles of the page it's not reflected in the appearance.
I'm looking at get_preset_classes
and get_css_variables
on the PHP side to see how that could be used for this.
Your team mentioned this in the call as well, and I'm not sure what this refers to. Can you tell me a little more about what you mean by visually representable? In the case of the nested PR, the rules are represented in
theme.json
are visually represented in the block controls. Is there another UI representation oftheme.json
/block.json
/per-section settings and styling rules beyond the block editor itself?
What we meant here is that, currently, configurations are easily representable in the Global Styles sidebar: there are general styles accessible via Typography, Colors, Layouts at the root of the sidebar drill-down menu, and then there are block-specific ones. There is room to improve that UX, but it's still straightforward.
A system of nested block rules would certainly stress the UI. One would have to drill down nested lists of blocks to determine how a block should be styles in the context of another. While not impossible to represent, this wouldn't be an experience to subject to the majority of users. Adding other signals like user roles to that system would be the nail on its coffin.
Thanks for the feedback @mcsf and @oandregal!
We have posted this PR as a result of it.
How to get the additional block settings injected as CSS attributes onto the editor? At the moment, we are able to select the setting (change the colour to red for example) from the editor's block panel. But, since the CSS attribute has not been injected into the styles of the page it's not reflected in the appearance.
@ingeniumed I am exploring the new blockEditor.useSetting.before
hook and was wondering if the above issue was ever resolved or if you came up with a way to easily accomplish this. Thanks!
I am exploring the new blockEditor.useSetting.before hook and was wondering if the above issue was ever resolved or if you came up with a way to easily accomplish this. Thanks!
As far as we're aware, there's still no official way to load CSS into the iframed block editor although there are some workarounds discussed in #38673. Additionally, the work being done in #46496 should make it easier to dynamically pull block selectors and generate the appropriate styling for the block editor.
That being said, the blockEditor.useSetting.before
hook still works in places that don't require style changes, like dynamically enabling/disabling block options. We've added an example of this over in https://github.com/WordPress/developer-blog-content/discussions/38#discussioncomment-5096753.
What problem does this address?
We want to be able to control what block settings are offered to the user in more ways than core currently allows.
I work in the space of building WordPress applications for enterprises and large organizations. For those of us in this space, this makes our end-user a bit different from the individual or small business that might benefit from granular design control over blocks.
Our users have the resources to hand theme design and development over to designers and developers. Our challenge isn't implementing a design in the block editor, but developing blocks that provide more targeted flexibility and options within an existing design system.
Our users aren't designing websites and themes in the block editor. They're assembling websites using blocks that have been designed and customized for them specifically. For the designers and developers making those blocks, governance is a primary concern.
Right now we can define block settings in theme.json like this.
As a simple example, let’s look at color.
Our global styles palette contains several color options. It looks like this:
We use the global styles
--wp--presets--color
object as a “theme” which defines the standard style tokens that our blocks will look for. Colors are named semantically and draw from the tokens of our design system which is housed in the--wp--custom
object for now—although we're working on being able to reference it from it's external npm package.We want to continue to use global styles as a way to give semantic structure to our styles. It mirrors how we think of our application while we're designing it and it makes "re-theming" a lot easier. It gives us a set number of variables that a block needs to support in order to be styled properly by the theme.
However, we don’t want all these colors to be available wherever a user is presented with a palette. We only want certain colors to be available in specific places and the number of available options can be overwhelming in situations where only a couple of options would be appropriate.
Limiting block settings is currently possible on a theme-wide level. You can override the theme palette within a block like this:
Here I’m overriding the palette to limit the colors available within the heading block to the “heading” option. If we had alternate heading colors we could also add those here. This makes blocks a lot easier to use, especially as they become more complex and designers need to consider how users will interact with them in the editor.
The trouble is that this only happens on a theme-wide level. We want to be able to define all possible options that a certain block supports and then narrow down what options are presented to a user in the block editor at any given time based on a few context-based factors:
Where is the block? what is its’ parent? what template is it in? what post type is it on? what block is immediately before and after it? etc.
These questions are ones we consider when designing and the answers to them can impact what options we make available to a user.
Who is interacting with it? what permissions does the user interacting with it have? are they a designer? are they just a copy editor?
This becomes clear with the example of type styles.
Similar to the global styles
--wp--presets--color
object we use the--wp--presets--font-sizes
object as a theme and name the tokens semantically. Here are all the options we currently have in our type system.There are a lot of them and all the text on the site is mapped to a font size here that corresponds to our design system. This is essential for us to be able to maintain control and consistency across all the various applications our type scale needs to serve.
Similar to color, we really don’t want to present all these options to a user every time they are asked to select a font size. Not only would it be overwhelming but it would present a lot of possibility for error and would force our user to either make a design decision or consult a style guide or a designer for help.
Going back to the example of our heading block, it potentially helps to narrow down the options on a theme level like this:
Here, I’m limiting the options to only the heading font sizes. This is an improvement, but still falls short of the user experience we need. Giving a user all the heading options still forces them to make a design decision or consult a style guide or designer. If we could further limit the options here in a way that responds to the questions above we would be able to design a user experience that limits the possibility for design error—one that lets our users focus on content creation instead of design.
In our custom
page-header
block, our heading block is limited to 3 size options. Right now we accomplish this by forgoing the core block and making that block totally custom. This has achieved the experience we need for our users but it comes at the expense of introducing complexity to our codebase and opting simple blocks out of any further improvements that come through core.What is your proposed solution?
Being able to pass a second set of settings for a block that only applies to a specified context would probably do what we need. Doing this in theme.json would be nice.
The theme.json settings for an implementation using core blocks might look something like this:
These are all the font-size options available to the heading block across the theme:
This would be the curated sub-set of font-size options available to the heading block when it is placed within the page-header block: