Skip to content
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

Allow themes to enqueue custom CSS variables via theme.json #25446

Merged
merged 9 commits into from
Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 49 additions & 13 deletions docs/designers-developers/developers/themes/theme-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
This is documentation for the current direction and work in progress about how themes can hook into the various sub-systems that the Block Editor provides.

- Rationale
- Presets become CSS Custom Properties
- Some block styles are managed
- Settings can be controlled per block
- CSS Custom Properties: presets & custom
- Some block styles are managed
- Specification
- Settings
- Styles
Expand All @@ -20,20 +20,18 @@ The Block Editor surface API has evolved at different velocities, and it's now a

This describes the current efforts to consolidate the various APIs into a single point – a `experimental-theme.json` file that should be located inside the root of the theme directory.

### Presets become CSS Custom Properties
### Settings can be controlled per block

The Block Editor already allows the control of specific settings such as alignment, drop cap, whether it's present in the inserter, etc at the block level. The goal is to surface these for themes to control at a block level.

Presets such as [color palettes](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-color-palettes), [font sizes](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-font-sizes), and [gradients](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-gradient-presets) will be enqueued as CSS Custom Properties for themes to use.
### CSS Custom Properties

These will be enqueued to the front-end and editor.
Presets such as [color palettes](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-color-palettes), [font sizes](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-font-sizes), and [gradients](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-gradient-presets) become CSS Custom Properties, and will be enqueued by the system for themes to use in both the front-end and the editors. There's also a mechanism to create your own CSS Custom Properties.

### Some block styles are managed

By providing the block style properties in a structured way, the Block Editor can "manage" the CSS that comes from different origins (user, theme, and core CSS), reducing the amount of CSS loaded in the page and preventing specificity wars due to the competing needs of the components involved (themes, blocks, plugins).

### Settings can be controlled per block

The Block Editor already allows the control of specific settings such as alignment, drop cap, whether it's present in the inserter, etc at the block level. The goal is to surface these for themes to control.

## Specification

The `experimental-theme.json` file is divided into sections known as "contexts", that represent a different CSS selector. For example, the `core/paragraph` context maps to `p` while `core/group` maps to `.wp-block-group`. In general, one block will map to a single context as in the cases mentioned. There are cases where one block can generate multiple contexts (different CSS selectors). For example, the heading block generates six different contexts (`core/heading/h1`, `core/heading/h2`, etc), one for each different selector (h1, h2, etc).
Expand All @@ -60,7 +58,8 @@ Every context has the same structure, divided in two sections: `settings` and `s
"settings": {
"color": [ ... ],
"typography": [ ... ],
"spacing": [ ... ]
"spacing": [ ... ],
"custom": [ ... ]
},
"styles": {
"color": { ... },
Expand Down Expand Up @@ -96,13 +95,14 @@ The settings section has the following structure and default values:
"customLineHeight": false, /* true to opt-in, as in add_theme_support( 'custom-line-height' ) */
"dropCap": true, /* false to opt-out */
"fontSizes": [ ... ], /* font size presets, as in add_theme_support('editor-font-sizes', ... ) */
}
},
"custom": { ... }
}
}
}
```

To retain backward compatibility, `add_theme_support` declarations are considered as well. If a theme uses `add_theme_support('disable-custom-colors')`, it'll be the same as set `global.settings.color.custom` to `false`. If the `experimental-theme.json` contains any settings, these will take precedence over the values declared via `add_theme_support`.
To retain backward compatibility, `add_theme_support` declarations are retrofit in the proper categories. If a theme uses `add_theme_support('disable-custom-colors')`, it'll be the same as set `global.settings.color.custom` to `false`. If the `experimental-theme.json` contains any settings, these will take precedence over the values declared via `add_theme_support`.

Settings can also be controlled by context, providing a more fine-grained control over what exists via `add_theme_support`. As an example, let's say that a theme author wants to enable custom colors for the paragraph block exclusively. This is how it'd be done:

Expand All @@ -126,7 +126,7 @@ Settings can also be controlled by context, providing a more fine-grained contro

Note, however, that not all settings are relevant for all contexts and the blocks they represent. The settings section provides an opt-in/opt-out mechanism for themes, but it's the block's responsibility to add support for the features that are relevant to it. For example, if a block doesn't implement the `dropCap` feature, a theme can't enable it for such a block through `experimental-theme.json`.

### Presets
#### Presets

Presets are part of the settings section. At the moment, they only work within the `global` context. Each preset value will generate a CSS Custom Property that will be added to the new stylesheet, which follow this naming schema: `--wp--preset--{preset-category}--{preset-slug}`.

Expand Down Expand Up @@ -190,6 +190,42 @@ The output to be enqueued will be:

The goal is that presets can be defined using this format, although, right now, the name property (used in the editor) can't be translated from this file. For that reason, and to maintain backward compatibility, the presets declared via `add_theme_support` will also generate the CSS Custom Properties. If the `experimental-theme.json` contains any presets, these will take precedence over the ones declared via `add_theme_support`.

#### Free-form CSS Custom Properties

In addition to create CSS Custom Properties for the presets, the theme.json also allows for themes to create their own, so they don't have to be enqueued separately. Any values declared within the `settings.custom` section will be transformed to CSS Custom Properties following this naming schema: `--wp--custom--<variable-name>`.

For example, for this input:

```json
{
"global": {
"settings": {
"custom": {
"base-font": 16,
"line-height": {
"small": 1.2,
"medium": 1.4,
"large": 1.8
}
}
}
}
}
```

The output will be:

```css
:root {
--wp--custom--base-font: 16;
--wp--custom--line-height--small: 1.2;
--wp--custom--line-height--medium: 1.4;
--wp--custom--line-height--large: 1.8;
}
```

Note that, the name of the variable is created by adding `--` in between each nesting level.

### Styles

Each block declares which style properties it exposes. This has been coined as "implicit style attributes" of the block. These properties are then used to automatically generate the UI controls for the block in the editor, as well as being available through the `experimental-theme.json` file for themes to target.
Expand Down
26 changes: 17 additions & 9 deletions lib/global-styles.php
Original file line number Diff line number Diff line change
Expand Up @@ -578,11 +578,9 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree ) {
continue;
}

// Create the CSS Custom Properties for the presets.
$computed_presets = array();
$presets_structure = gutenberg_experimental_global_styles_get_presets_structure();

$computed_presets = array();

// Extract the relevant preset info before converting them to CSS Custom Properties.
foreach ( $presets_structure as $token => $preset_meta ) {
$block_preset = gutenberg_experimental_get( $tree[ $block_name ]['settings'], $preset_meta['path'] );
if ( ! empty( $block_preset ) ) {
Expand All @@ -593,17 +591,26 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree ) {
}
}
}

$token = '--';
$prefix = '--wp--preset' . $token;
$css_variables = gutenberg_experimental_global_styles_get_css_vars( $computed_presets, $prefix, $token );
$token = '--';
$preset_prefix = '--wp--preset' . $token;
$preset_variables = gutenberg_experimental_global_styles_get_css_vars( $computed_presets, $preset_prefix, $token );

// Create the CSS Custom Properties that are specific to the theme.
$computed_theme_props = gutenberg_experimental_get( $tree[ $block_name ]['settings'], array( 'custom' ) );
$theme_props_prefix = '--wp--custom' . $token;
$theme_variables = gutenberg_experimental_global_styles_get_css_vars(
$computed_theme_props,
$theme_props_prefix,
$token
);

$stylesheet .= gutenberg_experimental_global_styles_resolver_styles(
$block_data[ $block_name ]['selector'],
$block_data[ $block_name ]['supports'],
array_merge(
gutenberg_experimental_global_styles_flatten_styles_tree( $tree[ $block_name ]['styles'] ),
$css_variables
$preset_variables,
$theme_variables
)
);
}
Expand Down Expand Up @@ -719,6 +726,7 @@ function gutenberg_experimental_global_styles_normalize_schema( $tree ) {
),
'settings' => array(
'color' => array(),
'custom' => array(),
Copy link
Member

Choose a reason for hiding this comment

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

The CSS variables are like styles e.g: one can do:

:root {
	--wp-test-var: 4;
	background-color: black;
}

Given that css variables are like styles wouldn't it make sense to set the variables together with the styles?

Copy link
Member Author

@oandregal oandregal Sep 18, 2020

Choose a reason for hiding this comment

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

Presets are also styles, though. I think it makes sense having presets & this custom thing together as they behave the same: name is generated based on the object shape, both generate CSS vars (so aren't attached to any style prop so won't do anything by themselves).

'typography' => array(),
'spacing' => array(),
),
Expand Down
28 changes: 28 additions & 0 deletions packages/edit-site/src/components/editor/global-styles-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,33 @@ export default ( blockData, baseTree, userTree ) => {
);
};

const flattenTree = ( input, prefix, token ) => {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to render the custom variables on the client? There is no UI to change the custom variables, so I guess we can just keep the ones set on the server?

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 I'll address in a follow-up PR.

let result = [];
Object.keys( input ).forEach( ( key ) => {
Copy link
Member

Choose a reason for hiding this comment

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

Minor non-blocking suggestion: I guess using reduce instead of Object.keys + forEach would simplify a little bit the code here.

const newKey = prefix + key.replace( '/', '-' );
const newLeaf = input[ key ];

if ( newLeaf instanceof Object ) {
const newPrefix = newKey + token;
result = [
...result,
...flattenTree( newLeaf, newPrefix, token ),
];
} else {
result.push( `${ newKey }: ${ newLeaf }` );
}
} );
return result;
};

const getCustomDeclarations = ( blockCustom ) => {
if ( Object.keys( blockCustom ).length === 0 ) {
return [];
}

return flattenTree( blockCustom, '--wp--custom--', '--' );
};

const getBlockSelector = ( selector ) => {
// Can we hook into the styles generation mechanism
// so we can avoid having to increase the class specificity here
Expand All @@ -118,6 +145,7 @@ export default ( blockData, baseTree, userTree ) => {
tree[ context ].styles
),
...getBlockPresetsDeclarations( tree[ context ].settings ),
...getCustomDeclarations( tree[ context ].settings.custom ),
];
if ( blockDeclarations.length > 0 ) {
styles.push(
Expand Down