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 6d73dca557b05..521c13081944c 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 6df4ed512d025..ee3471b89a620 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 ef87951483d91..27821109d56a2 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,218 +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 += `:where(${ 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 += `:where(${ 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 += `:where(${ 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 += `:where(${ 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'; @@ -1001,17 +1045,23 @@ export const toStyles = ( ':where(.wp-site-blocks) > :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 b842bc9fe75e4..e65da7afb78aa 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 e6227ea2b03e2..214d2ded96b2b 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 dad62bc0594a7..6f64ae9c55e85 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -45,7 +45,7 @@ const styleSupportKeys = [ SHADOW_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 0000000000000..ec1acd362e31b --- /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 }; +}