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

Global Styles: Try per block custom CSS #46571

Merged
merged 18 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 33 additions & 0 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,27 @@ public function get_settings() {
}
}

/**
* Processes the CSS, to apply nesting.
*
* @param string $css The CSS to process.
* @param string $selector The selector to nest.
*
* @return string The processed CSS.
*/
public function process_blocks_custom_css( $css, $selector ) {
Copy link
Member

Choose a reason for hiding this comment

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

Are we sure we want this method to be public?

Copy link
Member

Choose a reason for hiding this comment

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

Well... In the beginning, I made it public to make testing the method easier.
But the more I think about it, the more I like it... The method doesn't change anything in the object itself, so I don't see a reason to make it private, as it's pretty self-isolated. This way it will be possible for 3rd parties to use it in their own implementations if they choose to do something with nested CSS. In fact, we could even make it a static function and nothing would change. Perhaps it would even be preferable? 🤷

Copy link
Member

Choose a reason for hiding this comment

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

That's the reason I asked. If the method is public just for unit tests, then we can make it protected and test the expected output using get_stylesheet(). We already do this for global custom CSS.

If you think it could be helpful outside of this particular case, then sure, keeping it public makes sense.

Copy link
Member

Choose a reason for hiding this comment

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

This way it will be possible for 3rd parties to use it in their own implementations if they choose to do something with nested CSS. In fact, we could even make it a static function and nothing would change. Perhaps it would even be preferable?

Note WP_Theme_JSON and WP_Theme_JsON_Resolver are private classes, which means we're asking people not to use them. The API should provide public methods for any operation third-parties may need. See related conversation at #45171

Would you mind making this protected?

$processed_css = '';

// Split CSS nested rules.
$parts = explode( '&', $css );
foreach ( $parts as $part ) {
$processed_css .= ( ! str_contains( $part, '{' ) )
? trim( $selector ) . '{' . trim( $part ) . '}' // If the part doesn't contain braces, it applies to the root level.
: trim( $selector . $part ); // Prepend the selector, which effectively replaces the "&" character.
}
return $processed_css;
}

/**
* Returns the stylesheet that results of processing
* the theme.json structure this object represents.
Expand Down Expand Up @@ -1062,7 +1083,19 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets'

// Load the custom CSS last so it has the highest specificity.
if ( in_array( 'custom-css', $types, true ) ) {
// Add the global styles root CSS.
$stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) );

// Add the global styles block CSS.
if ( isset( $this->theme_json['styles']['blocks'] ) ) {
foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
$custom_block_css = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $name, 'css' ) );
if ( $custom_block_css ) {
$selector = static::$blocks_metadata[ $name ]['selector'];
$stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
}
}
}
}

return $stylesheet;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,19 @@ function updateConfigWithSeparator( config ) {
return config;
}

const processCSSNesting = ( css, blockSelector ) => {
let processedCSS = '';

// Split CSS nested rules.
const parts = css.split( '&' );
parts.forEach( ( part ) => {
processedCSS += ! part.includes( '{' )
? blockSelector + '{' + part + '}' // If the part doesn't contain braces, it applies to the root level.
: blockSelector + part; // Prepend the selector, which effectively replaces the "&" character.
} );
return processedCSS;
};

export function useGlobalStylesOutput() {
let { merged: mergedConfig } = useContext( GlobalStylesContext );

Expand All @@ -1014,10 +1027,12 @@ export function useGlobalStylesOutput() {
return [];
}
mergedConfig = updateConfigWithSeparator( mergedConfig );

const blockSelectors = getBlockSelectors(
getBlockTypes(),
getBlockStyles
);

const customProperties = toCustomProperties(
mergedConfig,
blockSelectors
Expand Down Expand Up @@ -1047,6 +1062,22 @@ export function useGlobalStylesOutput() {
},
];

// Loop through the blocks to check if there are custom CSS values.
// If there are, get the block selector and push the selector together with
// the CSS value to the 'stylesheets' array.
getBlockTypes().forEach( ( blockType ) => {
if ( mergedConfig.styles.blocks[ blockType.name ]?.css ) {
const selector = blockSelectors[ blockType.name ].selector;
stylesheets.push( {
css: processCSSNesting(
mergedConfig.styles.blocks[ blockType.name ]?.css,
selector
),
isGlobalStyles: true,
} );
}
} );

return [ stylesheets, mergedConfig.settings, filters ];
}, [
hasBlockGapSupport,
Expand Down
158 changes: 114 additions & 44 deletions packages/edit-site/src/components/global-styles/context-menu.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
/**
* WordPress dependencies
*/
import { __experimentalItemGroup as ItemGroup } from '@wordpress/components';
import { typography, border, color, layout } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import {
__experimentalItemGroup as ItemGroup,
__experimentalHStack as HStack,
__experimentalSpacer as Spacer,
FlexItem,
CardBody,
CardDivider,
} from '@wordpress/components';
import {
typography,
border,
color,
layout,
chevronLeft,
chevronRight,
} from '@wordpress/icons';
import { isRTL, __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';

/**
* Internal dependencies
Expand All @@ -14,6 +30,7 @@ import { useHasDimensionsPanel } from './dimensions-panel';
import { useHasTypographyPanel } from './typography-panel';
import { useHasVariationsPanel } from './variations-panel';
import { NavigationButtonAsItem } from './navigation-button';
import { IconWithCurrentColor } from './icon-with-current-color';
import { ScreenVariations } from './screen-variations';

function ContextMenu( { name, parentMenu = '' } ) {
Expand All @@ -24,48 +41,101 @@ function ContextMenu( { name, parentMenu = '' } ) {
const hasLayoutPanel = hasDimensionsPanel;
const hasVariationsPanel = useHasVariationsPanel( name, parentMenu );

const { canEditCSS } = useSelect( ( select ) => {
carolinan marked this conversation as resolved.
Show resolved Hide resolved
const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } =
select( coreStore );

const globalStylesId = __experimentalGetCurrentGlobalStylesId();
const globalStyles = globalStylesId
? getEntityRecord( 'root', 'globalStyles', globalStylesId )
: undefined;

return {
canEditCSS:
!! globalStyles?._links?.[ 'wp:action-edit-css' ] ?? false,
};
}, [] );

const isBlocksPanel =
parentMenu.includes( 'blocks' ) &&
! parentMenu.includes( 'variations' );

return (
<ItemGroup>
{ hasTypographyPanel && (
<NavigationButtonAsItem
icon={ typography }
path={ parentMenu + '/typography' }
aria-label={ __( 'Typography styles' ) }
>
{ __( 'Typography' ) }
</NavigationButtonAsItem>
) }
{ hasColorPanel && (
<NavigationButtonAsItem
icon={ color }
path={ parentMenu + '/colors' }
aria-label={ __( 'Colors styles' ) }
>
{ __( 'Colors' ) }
</NavigationButtonAsItem>
) }
{ hasBorderPanel && (
<NavigationButtonAsItem
icon={ border }
path={ parentMenu + '/border' }
aria-label={ __( 'Border styles' ) }
>
{ __( 'Border' ) }
</NavigationButtonAsItem>
) }
{ hasLayoutPanel && (
<NavigationButtonAsItem
icon={ layout }
path={ parentMenu + '/layout' }
aria-label={ __( 'Layout styles' ) }
>
{ __( 'Layout' ) }
</NavigationButtonAsItem>
) }
{ hasVariationsPanel && (
<ScreenVariations name={ name } path={ parentMenu } />
) }
</ItemGroup>
<>
<ItemGroup>
{ hasTypographyPanel && (
<NavigationButtonAsItem
icon={ typography }
path={ parentMenu + '/typography' }
aria-label={ __( 'Typography styles' ) }
>
{ __( 'Typography' ) }
</NavigationButtonAsItem>
) }
{ hasColorPanel && (
<NavigationButtonAsItem
icon={ color }
path={ parentMenu + '/colors' }
aria-label={ __( 'Colors styles' ) }
>
{ __( 'Colors' ) }
</NavigationButtonAsItem>
) }
{ hasBorderPanel && (
<NavigationButtonAsItem
icon={ border }
path={ parentMenu + '/border' }
aria-label={ __( 'Border styles' ) }
>
{ __( 'Border' ) }
</NavigationButtonAsItem>
) }
{ hasLayoutPanel && (
<NavigationButtonAsItem
icon={ layout }
path={ parentMenu + '/layout' }
aria-label={ __( 'Layout styles' ) }
>
{ __( 'Layout' ) }
</NavigationButtonAsItem>
) }
{ hasVariationsPanel && (
<ScreenVariations name={ name } path={ parentMenu } />
) }
{ isBlocksPanel && canEditCSS && (
<>
<CardDivider />
<CardBody>
<Spacer as="p" paddingTop={ 2 } marginBottom={ 4 }>
{ __(
'Add your own CSS to customize the block appearance.'
) }
</Spacer>
<ItemGroup>
<NavigationButtonAsItem
path={ parentMenu + '/css' }
aria-label={ __( 'Additional block CSS' ) }
>
<HStack justify="space-between">
<FlexItem>
{ __( 'Additional block CSS' ) }
</FlexItem>
<IconWithCurrentColor
icon={
isRTL()
? chevronLeft
: chevronRight
}
/>
</HStack>
</NavigationButtonAsItem>
</ItemGroup>
</CardBody>
<CardDivider />
</>
) }
</ItemGroup>
</>
);
}

Expand Down
9 changes: 6 additions & 3 deletions packages/edit-site/src/components/global-styles/custom-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import { experiments as blockEditorExperiments } from '@wordpress/block-editor';
import { unlock } from '../../experiments';

const { useGlobalStyle } = unlock( blockEditorExperiments );
function CustomCSSControl( { blockName } ) {
// If blockName is defined, we are customizing CSS at the block level:
// styles.blocks.blockName.css
const block = !! blockName ? blockName : null;

function CustomCSSControl() {
const [ customCSS, setCustomCSS ] = useGlobalStyle( 'css' );
const [ themeCSS ] = useGlobalStyle( 'css', null, 'base' );
const [ customCSS, setCustomCSS ] = useGlobalStyle( 'css', block );
const [ themeCSS ] = useGlobalStyle( 'css', block, 'base' );
const ignoreThemeCustomCSS = '/* IgnoreThemeCustomCSS */';

// If there is custom css from theme.json show it in the edit box
Expand Down
32 changes: 23 additions & 9 deletions packages/edit-site/src/components/global-styles/screen-css.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { sprintf, __ } from '@wordpress/i18n';
import { __experimentalVStack as VStack } from '@wordpress/components';
import { getBlockType } from '@wordpress/blocks';

/**
* Internal dependencies
Expand All @@ -11,19 +12,32 @@ import ScreenHeader from './header';
import Subtitle from './subtitle';
import CustomCSSControl from './custom-css';

function ScreenCSS() {
function ScreenCSS( { name } ) {
// If name is defined, we are customizing CSS at the block level.
// Display the block title in the description.
const blockType = getBlockType( name );
const title = blockType?.title;

const description =
title !== undefined
? sprintf(
// translators: %s: is the name of a block e.g., 'Image' or 'Table'.
__(
'Add your own CSS to customize the appearance of the %s block.'
),
title
)
: __(
'Add your own CSS to customize the appearance and layout of your site.'
);

return (
<>
<ScreenHeader
title={ __( 'CSS' ) }
description={ __(
'Add your own CSS to customize the appearance and layout of your site.'
) }
/>
<ScreenHeader title={ __( 'CSS' ) } description={ description } />
<div className="edit-site-global-styles-screen-css">
<VStack spacing={ 3 }>
<Subtitle>{ __( 'ADDITIONAL CSS' ) }</Subtitle>
<CustomCSSControl />
<CustomCSSControl blockName={ name } />
</VStack>
</div>
</>
Expand Down
7 changes: 4 additions & 3 deletions packages/edit-site/src/components/global-styles/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ function ContextScreens( { name, parentMenu = '' } ) {
<ScreenLayout name={ name } variationPath={ variationPath } />
</GlobalStylesNavigationScreen>

<GlobalStylesNavigationScreen path={ parentMenu + '/css' }>
<ScreenCSS name={ name } />
</GlobalStylesNavigationScreen>

{ !! blockStyleVariations?.length && (
<BlockStylesNavigationScreens
blockStyles={ blockStyleVariations }
Expand Down Expand Up @@ -280,9 +284,6 @@ function GlobalStylesUI( { isStyleBookOpened, onCloseStyleBook } ) {
{ isStyleBookOpened && (
<GlobalStylesStyleBook onClose={ onCloseStyleBook } />
) }
<GlobalStylesNavigationScreen path="/css">
<ScreenCSS />
</GlobalStylesNavigationScreen>
</NavigatorProvider>
);
}
Expand Down
Loading