From cde41e79810d2e6fe4aa4d124d36678ea5ca6883 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:50:37 +1000 Subject: [PATCH] Start getting the variation styles generated in the editor This still needs to bring in the lowering of selector specificity for block styles on the JS side as well as fix the "root" variation styles not being generated like the frontend --- .../block-styles/use-styles-for-block.js | 16 +- .../src/components/global-styles/index.js | 2 + .../global-styles/use-global-styles-output.js | 426 ++++++++++-------- .../src/components/global-styles/utils.js | 4 + packages/block-editor/src/hooks/index.js | 2 + packages/block-editor/src/hooks/style.js | 2 +- packages/block-editor/src/hooks/variation.js | 82 ++++ 7 files changed, 343 insertions(+), 191 deletions(-) create mode 100644 packages/block-editor/src/hooks/variation.js diff --git a/packages/block-editor/src/components/block-styles/use-styles-for-block.js b/packages/block-editor/src/components/block-styles/use-styles-for-block.js index 6d73dca557b058..521c13081944ce 100644 --- a/packages/block-editor/src/components/block-styles/use-styles-for-block.js +++ b/packages/block-editor/src/components/block-styles/use-styles-for-block.js @@ -15,6 +15,7 @@ import { useMemo } from '@wordpress/element'; */ import { getActiveStyle, getRenderedStyles, replaceActiveStyle } from './utils'; import { store as blockEditorStore } from '../../store'; +import { cleanEmptyObject } from '../../hooks/utils'; /** * @@ -67,11 +68,13 @@ export default function useStylesForBlocks( { clientId, onSwitch } ) { blockType, styles: getBlockStyles( block.name ), className: block.attributes.className || '', + attributes: block.attributes, }; }; - const { styles, block, blockType, className } = useSelect( selector, [ - clientId, - ] ); + const { styles, block, blockType, className, attributes } = useSelect( + selector, + [ clientId ] + ); const { updateBlockAttributes } = useDispatch( blockEditorStore ); const stylesToRender = getRenderedStyles( styles ); const activeStyle = getActiveStyle( stylesToRender, className ); @@ -83,8 +86,15 @@ export default function useStylesForBlocks( { clientId, onSwitch } ) { activeStyle, style ); + + const newStyleAttribute = cleanEmptyObject( { + ...attributes.style, + variation: style.name !== 'default' ? style.name : undefined, + } ); + updateBlockAttributes( clientId, { className: styleClassName, + style: newStyleAttribute, } ); onSwitch(); }; diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 65392a7636c442..29256ffff461a6 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -7,6 +7,8 @@ export { export { getBlockCSSSelector } from './get-block-css-selector'; export { getLayoutStyles, + getBlockSelectors, + toStyles, useGlobalStylesOutput, useGlobalStylesOutputWithConfig, } from './use-global-styles-output'; diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 4e8ea6dc0ff95b..931e2e8ffb519c 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -775,216 +775,262 @@ export const toStyles = ( hasBlockGapSupport, hasFallbackGapSupport, disableLayoutStyles = false, - isTemplate = true + isTemplate = true, + styleOptions = undefined ) => { const nodesWithStyles = getNodesWithStyles( tree, blockSelectors ); const nodesWithSettings = getNodesWithSettings( tree, blockSelectors ); const useRootPaddingAlign = tree?.settings?.useRootPaddingAwareAlignments; const { contentSize, wideSize } = tree?.settings?.layout || {}; + const options = { + blockGap: true, + blockStyles: true, + layoutStyles: true, + marginReset: true, + presets: true, + rootPadding: true, + scopeSelector: undefined, + ...styleOptions, + }; + const hasBodyStyles = + options.marginReset || options.rootPadding || options.layoutStyles; - /* - * Reset default browser margin on the root body element. - * This is set on the root selector **before** generating the ruleset - * from the `theme.json`. This is to ensure that if the `theme.json` declares - * `margin` in its `spacing` declaration for the `body` element then these - * user-generated values take precedence in the CSS cascade. - * @link https://github.com/WordPress/gutenberg/issues/36147. - */ - let ruleset = 'body {margin: 0;'; - - if ( contentSize ) { - ruleset += ` --wp--style--global--content-size: ${ contentSize };`; - } - - if ( wideSize ) { - ruleset += ` --wp--style--global--wide-size: ${ wideSize };`; - } + let ruleset = ''; - // Root padding styles should only be output for full templates, not patterns or template parts. - if ( useRootPaddingAlign && isTemplate ) { + if ( hasBodyStyles ) { /* - * These rules reproduce the ones from https://github.com/WordPress/gutenberg/blob/79103f124925d1f457f627e154f52a56228ed5ad/lib/class-wp-theme-json-gutenberg.php#L2508 - * almost exactly, but for the selectors that target block wrappers in the front end. This code only runs in the editor, so it doesn't need those selectors. + * Reset default browser margin on the root body element. + * This is set on the root selector **before** generating the ruleset + * from the `theme.json`. This is to ensure that if the `theme.json` declares + * `margin` in its `spacing` declaration for the `body` element then these + * user-generated values take precedence in the CSS cascade. + * @link https://github.com/WordPress/gutenberg/issues/36147. */ - ruleset += `padding-right: 0; padding-left: 0; padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom) } - .has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); } - .has-global-padding :where(.has-global-padding:not(.wp-block-block)) { padding-right: 0; padding-left: 0; } - .has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); } - .has-global-padding :where(.has-global-padding:not(.wp-block-block)) > .alignfull { margin-right: 0; margin-left: 0; } - .has-global-padding > .alignfull:where(:not(.has-global-padding):not(.is-layout-flex):not(.is-layout-grid)) > :where(.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); } - .has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where(.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0;`; - } + ruleset = 'body {margin: 0;'; - ruleset += '}'; + if ( options.layoutStyles && contentSize ) { + ruleset += ` --wp--style--global--content-size: ${ contentSize };`; + } - nodesWithStyles.forEach( - ( { - selector, - duotoneSelector, - styles, - fallbackGapValue, - hasLayoutSupport, - featureSelectors, - styleVariationSelectors, - } ) => { - // Process styles for block support features with custom feature level - // CSS selectors set. - if ( featureSelectors ) { - const featureDeclarations = getFeatureDeclarations( - featureSelectors, - styles - ); + if ( options.layoutStyles && wideSize ) { + ruleset += ` --wp--style--global--wide-size: ${ wideSize };`; + } - Object.entries( featureDeclarations ).forEach( - ( [ cssSelector, declarations ] ) => { - if ( declarations.length ) { - const rules = declarations.join( ';' ); - ruleset += `${ cssSelector }{${ rules };}`; - } - } - ); - } + // Root padding styles should only be output for full templates, not patterns or template parts. + if ( options.rootPadding && useRootPaddingAlign && isTemplate ) { + /* + * These rules reproduce the ones from https://github.com/WordPress/gutenberg/blob/79103f124925d1f457f627e154f52a56228ed5ad/lib/class-wp-theme-json-gutenberg.php#L2508 + * almost exactly, but for the selectors that target block wrappers in the front end. This code only runs in the editor, so it doesn't need those selectors. + */ + ruleset += `padding-right: 0; padding-left: 0; padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom) } + .has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); } + .has-global-padding :where(.has-global-padding:not(.wp-block-block)) { padding-right: 0; padding-left: 0; } + .has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); } + .has-global-padding :where(.has-global-padding:not(.wp-block-block)) > .alignfull { margin-right: 0; margin-left: 0; } + .has-global-padding > .alignfull:where(:not(.has-global-padding):not(.is-layout-flex):not(.is-layout-grid)) > :where(.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); } + .has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where(.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0;`; + } - if ( styleVariationSelectors ) { - Object.entries( styleVariationSelectors ).forEach( - ( [ styleVariationName, styleVariationSelector ] ) => { - const styleVariations = - styles?.variations?.[ styleVariationName ]; - if ( styleVariations ) { - // If the block uses any custom selectors for block support, add those first. - if ( featureSelectors ) { - const featureDeclarations = - getFeatureDeclarations( - featureSelectors, - styleVariations - ); + ruleset += '}'; + } - Object.entries( featureDeclarations ).forEach( - ( [ baseSelector, declarations ] ) => { - if ( declarations.length ) { - const cssSelector = - concatFeatureVariationSelectorString( - baseSelector, - styleVariationSelector - ); - const rules = - declarations.join( ';' ); - ruleset += `${ cssSelector }{${ rules };}`; - } - } + if ( options.blockStyles ) { + nodesWithStyles.forEach( + ( { + selector, + duotoneSelector, + styles, + fallbackGapValue, + hasLayoutSupport, + featureSelectors, + styleVariationSelectors, + } ) => { + // Process styles for block support features with custom feature level + // CSS selectors set. + if ( featureSelectors ) { + const featureDeclarations = getFeatureDeclarations( + featureSelectors, + styles + ); + + Object.entries( featureDeclarations ).forEach( + ( [ cssSelector, declarations ] ) => { + if ( declarations.length ) { + const rules = declarations.join( ';' ); + const scopedSelector = scopeSelector( + options.scopeSelector, + cssSelector ); + ruleset += `${ scopedSelector }{${ rules };}`; } + } + ); + } - // Otherwise add regular selectors. - const styleVariationDeclarations = - getStylesDeclarations( - styleVariations, - styleVariationSelector, - useRootPaddingAlign, - tree - ); - if ( styleVariationDeclarations.length ) { - ruleset += `${ styleVariationSelector }{${ styleVariationDeclarations.join( - ';' - ) };}`; + if ( styleVariationSelectors ) { + Object.entries( styleVariationSelectors ).forEach( + ( [ styleVariationName, styleVariationSelector ] ) => { + const styleVariations = + styles?.variations?.[ styleVariationName ]; + if ( styleVariations ) { + // If the block uses any custom selectors for block support, add those first. + if ( featureSelectors ) { + const featureDeclarations = + getFeatureDeclarations( + featureSelectors, + styleVariations + ); + + Object.entries( + featureDeclarations + ).forEach( + ( [ baseSelector, declarations ] ) => { + if ( declarations.length ) { + const cssSelector = + concatFeatureVariationSelectorString( + baseSelector, + styleVariationSelector + ); + const scopedSelector = + scopeSelector( + options.scopeSelector, + cssSelector + ); + const rules = + declarations.join( ';' ); + ruleset += `${ scopedSelector }{${ rules };}`; + } + } + ); + } + + // Otherwise add regular selectors. + const styleVariationDeclarations = + getStylesDeclarations( + styleVariations, + styleVariationSelector, + useRootPaddingAlign, + tree + ); + if ( styleVariationDeclarations.length ) { + ruleset += `${ styleVariationSelector }{${ styleVariationDeclarations.join( + ';' + ) };}`; + } } } + ); + } + + // Process duotone styles. + if ( duotoneSelector ) { + const duotoneStyles = {}; + if ( styles?.filter ) { + duotoneStyles.filter = styles.filter; + delete styles.filter; } - ); - } + const duotoneDeclarations = + getStylesDeclarations( duotoneStyles ); + if ( duotoneDeclarations.length ) { + const scopedSelector = scopeSelector( + options.scopeSelector, + duotoneSelector + ); + ruleset += `${ scopedSelector }{${ duotoneDeclarations.join( + ';' + ) };}`; + } + } - // Process duotone styles. - if ( duotoneSelector ) { - const duotoneStyles = {}; - if ( styles?.filter ) { - duotoneStyles.filter = styles.filter; - delete styles.filter; + // Process blockGap and layout styles. + if ( + ! disableLayoutStyles && + ( ROOT_BLOCK_SELECTOR === selector || hasLayoutSupport ) + ) { + ruleset += getLayoutStyles( { + style: styles, + selector, + hasBlockGapSupport, + hasFallbackGapSupport, + fallbackGapValue, + } ); } - const duotoneDeclarations = - getStylesDeclarations( duotoneStyles ); - if ( duotoneDeclarations.length ) { - ruleset += `${ duotoneSelector }{${ duotoneDeclarations.join( + + // Process the remaining block styles (they use either normal block class or __experimentalSelector). + const declarations = getStylesDeclarations( + styles, + selector, + useRootPaddingAlign, + tree, + isTemplate + ); + if ( declarations?.length ) { + const scopedSelector = scopeSelector( + options.scopeSelector, + selector + ); + ruleset += `${ scopedSelector }{${ declarations.join( ';' ) };}`; } - } - // Process blockGap and layout styles. - if ( - ! disableLayoutStyles && - ( ROOT_BLOCK_SELECTOR === selector || hasLayoutSupport ) - ) { - ruleset += getLayoutStyles( { - style: styles, - selector, - hasBlockGapSupport, - hasFallbackGapSupport, - fallbackGapValue, - } ); - } - - // Process the remaining block styles (they use either normal block class or __experimentalSelector). - const declarations = getStylesDeclarations( - styles, - selector, - useRootPaddingAlign, - tree, - isTemplate - ); - if ( declarations?.length ) { - ruleset += `${ selector }{${ declarations.join( ';' ) };}`; - } + // Check for pseudo selector in `styles` and handle separately. + const pseudoSelectorStyles = Object.entries( styles ).filter( + ( [ key ] ) => key.startsWith( ':' ) + ); - // Check for pseudo selector in `styles` and handle separately. - const pseudoSelectorStyles = Object.entries( styles ).filter( - ( [ key ] ) => key.startsWith( ':' ) - ); + if ( pseudoSelectorStyles?.length ) { + pseudoSelectorStyles.forEach( + ( [ pseudoKey, pseudoStyle ] ) => { + const pseudoDeclarations = + getStylesDeclarations( pseudoStyle ); - if ( pseudoSelectorStyles?.length ) { - pseudoSelectorStyles.forEach( - ( [ pseudoKey, pseudoStyle ] ) => { - const pseudoDeclarations = - getStylesDeclarations( pseudoStyle ); + if ( ! pseudoDeclarations?.length ) { + return; + } - if ( ! pseudoDeclarations?.length ) { - return; - } + // `selector` maybe provided in a form + // where block level selectors have sub element + // selectors appended to them as a comma separated + // string. + // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`; + // Split and append pseudo selector to create + // the proper rules to target the elements. + const _selector = selector + .split( ',' ) + .map( ( sel ) => sel + pseudoKey ) + .join( ',' ); + const scopedSelector = scopeSelector( + options.scopeSelector, + _selector + ); - // `selector` maybe provided in a form - // where block level selectors have sub element - // selectors appended to them as a comma separated - // string. - // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`; - // Split and append pseudo selector to create - // the proper rules to target the elements. - const _selector = selector - .split( ',' ) - .map( ( sel ) => sel + pseudoKey ) - .join( ',' ); - - const pseudoRule = `${ _selector }{${ pseudoDeclarations.join( - ';' - ) };}`; + const pseudoRule = `${ scopedSelector }{${ pseudoDeclarations.join( + ';' + ) };}`; - ruleset += pseudoRule; - } - ); + ruleset += pseudoRule; + } + ); + } } - } - ); + ); + } - /* Add alignment / layout styles */ - ruleset = - ruleset + - '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; - ruleset = - ruleset + - '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; - ruleset = - ruleset + - '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - if ( hasBlockGapSupport ) { + if ( options.layoutStyles ) { + /* Add alignment / layout styles */ + ruleset = + ruleset + + '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; + ruleset = + ruleset + + '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; + ruleset = + ruleset + + '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + } + + if ( hasBlockGapSupport && options.blockGap ) { // Use fallback of `0.5em` just in case, however if there is blockGap support, there should nearly always be a real value. const gapValue = getGapCSSValue( tree?.styles?.spacing?.blockGap ) || '0.5em'; @@ -999,17 +1045,23 @@ export const toStyles = ( ':where(.wp-site-blocks) > :last-child:last-child { margin-block-end: 0; }'; } - nodesWithSettings.forEach( ( { selector, presets } ) => { - if ( ROOT_BLOCK_SELECTOR === selector ) { - // Do not add extra specificity for top-level classes. - selector = ''; - } + if ( options.presets ) { + nodesWithSettings.forEach( ( { selector, presets } ) => { + if ( ROOT_BLOCK_SELECTOR === selector ) { + // Do not add extra specificity for top-level classes. + selector = ''; + } - const classes = getPresetsClasses( selector, presets ); - if ( classes.length > 0 ) { - ruleset += classes; - } - } ); + const classes = getPresetsClasses( + scopeSelector( options.scopeSelector, selector ), + presets + ); + + if ( classes.length > 0 ) { + ruleset += classes; + } + } ); + } return ruleset; }; diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index b842bc9fe75e4d..e65da7afb78aa8 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -387,6 +387,10 @@ export function getValueFromVariable( features, blockName, variable ) { * @return {string} Scoped selector. */ export function scopeSelector( scope, selector ) { + if ( ! scope || ! selector ) { + return selector; + } + const scopes = scope.split( ',' ); const selectors = selector.split( ',' ); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index e6227ea2b03e2e..214d2ded96b2b4 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -22,6 +22,7 @@ import fontFamily from './font-family'; import fontSize from './font-size'; import border from './border'; import position from './position'; +import variation from './variation'; import layout from './layout'; import childLayout from './layout-child'; import contentLockUI from './content-lock-ui'; @@ -54,6 +55,7 @@ createBlockListBlockFilter( [ fontSize, border, position, + variation, childLayout, ] ); createBlockSaveFilter( [ diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 42fe431a40b242..edbe707cd810fc 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -50,7 +50,7 @@ const styleSupportKeys = [ SPACING_SUPPORT_KEY, ]; -const hasStyleSupport = ( nameOrType ) => +export const hasStyleSupport = ( nameOrType ) => styleSupportKeys.some( ( key ) => hasBlockSupport( nameOrType, key ) ); /** diff --git a/packages/block-editor/src/hooks/variation.js b/packages/block-editor/src/hooks/variation.js new file mode 100644 index 00000000000000..ec1acd362e31bb --- /dev/null +++ b/packages/block-editor/src/hooks/variation.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { getBlockTypes, store as blocksStore } from '@wordpress/blocks'; +import { useInstanceId } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useStyleOverride } from './utils'; +import { toStyles, getBlockSelectors } from '../components/global-styles'; +import { store as blockEditorStore } from '../store'; + +export default { + hasSupport: () => true, // TODO: Work out what the eligibility here should be. + attributeKeys: [ 'style' ], + useBlockProps, +}; + +function useBlockSyleVariation( name, variation ) { + const { settings, styles } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return { + settings: getSettings().__experimentalFeatures, + styles: getSettings().__experimentalStyles, + }; + }, [] ); + + return { + settings, + styles: styles?.blocks?.[ name ]?.variations?.[ variation ], + }; +} + +function useBlockProps( { name, style } ) { + const variation = style?.variation; + const className = `is-style-${ variation }-${ useInstanceId( + useBlockProps + ) }`; + + const { settings, styles } = useBlockSyleVariation( name, variation ); + + const getBlockStyles = useSelect( ( select ) => { + return select( blocksStore ).getBlockStyles; + }, [] ); + + const variationStyles = useMemo( () => { + const variationConfig = { settings, styles }; + const blockSelectors = getBlockSelectors( + getBlockTypes(), + getBlockStyles + ); + const hasBlockGapSupport = false; + const hasFallbackGapSupport = true; + const disableLayoutStyles = true; + const isTemplate = true; + + return toStyles( + variationConfig, + blockSelectors, + hasBlockGapSupport, + hasFallbackGapSupport, + disableLayoutStyles, + isTemplate, + { + blockGap: false, + blockStyles: true, + layoutStyles: false, + marginReset: false, + presets: false, + rootPadding: false, + scopeSelector: `.${ className }`, + } + ); + }, [ variation, settings, styles, className ] ); + + useStyleOverride( { css: variationStyles } ); + + return { className }; +}