diff --git a/lib/compat/wordpress-6.2/block-editor-settings.php b/lib/compat/wordpress-6.2/block-editor-settings.php index 7323d34eb667ce..7f40429acf3076 100644 --- a/lib/compat/wordpress-6.2/block-editor-settings.php +++ b/lib/compat/wordpress-6.2/block-editor-settings.php @@ -15,11 +15,12 @@ function gutenberg_get_block_editor_settings_6_2( $settings ) { if ( wp_theme_has_theme_json() ) { // Add the custom CSS as separate style sheet so any invalid CSS entered by users does not break other global styles. - $settings['styles'][] = array( + $settings['styles'][] = array( 'css' => gutenberg_get_global_stylesheet( array( 'custom-css' ) ), '__unstableType' => 'user', 'isGlobalStyles' => true, ); + $settings['__experimentalStyles'] = wp_get_global_styles(); } return $settings; diff --git a/lib/experimental/class-wp-rest-block-editor-settings-controller.php b/lib/experimental/class-wp-rest-block-editor-settings-controller.php index 7031a14e89ccac..43735477b9ca14 100644 --- a/lib/experimental/class-wp-rest-block-editor-settings-controller.php +++ b/lib/experimental/class-wp-rest-block-editor-settings-controller.php @@ -153,7 +153,7 @@ public function get_item_schema() { '__experimentalStyles' => array( 'description' => __( 'Styles consolidated from core, theme, and user origins.', 'gutenberg' ), 'type' => 'object', - 'context' => array( 'mobile' ), + 'context' => array( 'post-editor', 'widgets-editor', 'mobile' ), ), '__experimentalEnableQuoteBlockV2' => array( diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 03ce8f3880ad32..a674b8c931455d 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -164,3 +164,4 @@ export { default as __experimentalInspectorPopoverHeader } from './inspector-pop export { default as BlockEditorProvider } from './provider'; export { default as useSetting } from './use-setting'; +export { default as __experimentalUseStyle } from './use-style'; diff --git a/packages/block-editor/src/components/line-height-control/index.js b/packages/block-editor/src/components/line-height-control/index.js index 670d6fac37e8df..4c8c1ac83504c1 100644 --- a/packages/block-editor/src/components/line-height-control/index.js +++ b/packages/block-editor/src/components/line-height-control/index.js @@ -21,6 +21,7 @@ const LineHeightControl = ( { /** Start opting into the new margin-free styles that will become the default in a future version. */ __nextHasNoMarginBottom = false, __unstableInputWidth = '60px', + placeholder = BASE_DEFAULT_VALUE, ...otherProps } ) => { const isDefined = isLineHeightDefined( lineHeight ); @@ -95,7 +96,7 @@ const LineHeightControl = ( { __unstableStateReducer={ stateReducer } onChange={ onChange } label={ __( 'Line height' ) } - placeholder={ BASE_DEFAULT_VALUE } + placeholder={ placeholder } step={ STEP } value={ value } min={ 0 } diff --git a/packages/block-editor/src/components/use-style/index.js b/packages/block-editor/src/components/use-style/index.js new file mode 100644 index 00000000000000..b7d30274502a70 --- /dev/null +++ b/packages/block-editor/src/components/use-style/index.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useBlockEditContext } from '../block-edit'; +import { store as blockEditorStore } from '../../store'; +import { getValueFromVariable } from '../../utils/style-variable-resolution'; + +/** + * Hook that retrieves the global styles of a block. + * It works with nested objects using by finding the value at path. + * + * @param {string|Array} path The path to the setting. + * + * @return {any} Returns the style value defined for the path. + * + * @example + * ```js + * const backgroundColor = useStyle( 'color.background' ); + * ``` + */ +export default function useStyle( path ) { + const { name: blockName } = useBlockEditContext(); + + const settings = useSelect( ( select ) => { + return select( blockEditorStore ).getSettings(); + }, [] ); + const stylesForBlock = get( settings, [ + '__experimentalStyles', + 'blocks', + blockName, + ] ); + const value = get( stylesForBlock, path ); + return useMemo( () => { + return getValueFromVariable( + settings.__experimentalFeatures, + blockName, + value + ); + }, [ settings.__experimentalFeatures, blockName, value ] ); +} diff --git a/packages/block-editor/src/hooks/line-height.js b/packages/block-editor/src/hooks/line-height.js index c8397d850a1e55..99a6b0ccf90d4d 100644 --- a/packages/block-editor/src/hooks/line-height.js +++ b/packages/block-editor/src/hooks/line-height.js @@ -9,6 +9,7 @@ import { hasBlockSupport } from '@wordpress/blocks'; import LineHeightControl from '../components/line-height-control'; import { cleanEmptyObject } from './utils'; import useSetting from '../components/use-setting'; +import useStyle from '../components/use-style'; export const LINE_HEIGHT_SUPPORT_KEY = 'typography.lineHeight'; @@ -24,6 +25,7 @@ export function LineHeightEdit( props ) { attributes: { style }, setAttributes, } = props; + const defaultLineHeight = useStyle( [ 'typography', 'lineHeight' ] ); const onChange = ( newLineHeightValue ) => { const newStyle = { @@ -43,6 +45,7 @@ export function LineHeightEdit( props ) { value={ style?.typography?.lineHeight } onChange={ onChange } size="__unstable-large" + placeholder={ defaultLineHeight } /> ); } diff --git a/packages/block-editor/src/utils/index.js b/packages/block-editor/src/utils/index.js index af45111759699c..8513946456910e 100644 --- a/packages/block-editor/src/utils/index.js +++ b/packages/block-editor/src/utils/index.js @@ -1,3 +1,4 @@ export { default as transformStyles } from './transform-styles'; export * from './block-variation-transforms'; export { default as getPxFromCssUnit } from './parse-css-unit-to-px'; +export { getValueFromVariable as __experimentalGetValueFromVariable } from './style-variable-resolution'; diff --git a/packages/block-editor/src/utils/style-variable-resolution.js b/packages/block-editor/src/utils/style-variable-resolution.js new file mode 100644 index 00000000000000..2d786a2ba11426 --- /dev/null +++ b/packages/block-editor/src/utils/style-variable-resolution.js @@ -0,0 +1,380 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * Internal dependencies + */ +import { getComputedFluidTypographyValue } from '../components/font-sizes'; + +// All of this has been copied from the edit/site package for now. + +export function getTypographyFontSizeValue( preset, typographySettings ) { + const { size: defaultSize } = preset; + + /* + * Catches falsy values and 0/'0'. + * Fluid calculations cannot be performed on 0. + */ + if ( ! defaultSize || '0' === defaultSize ) { + return defaultSize; + } + + if ( + ! typographySettings?.fluid || + ( typeof typographySettings?.fluid === 'object' && + Object.keys( typographySettings.fluid ).length === 0 ) + ) { + return defaultSize; + } + + // A font size has explicitly bypassed fluid calculations. + if ( false === preset?.fluid ) { + return defaultSize; + } + + const fluidTypographySettings = + typeof typographySettings?.fluid === 'object' + ? typographySettings?.fluid + : {}; + + const fluidFontSizeValue = getComputedFluidTypographyValue( { + minimumFontSize: preset?.fluid?.min, + maximumFontSize: preset?.fluid?.max, + fontSize: defaultSize, + minimumFontSizeLimit: fluidTypographySettings?.minFontSize, + } ); + + if ( !! fluidFontSizeValue ) { + return fluidFontSizeValue; + } + + return defaultSize; +} + +/* Supporting data. */ +export const ROOT_BLOCK_NAME = 'root'; +export const ROOT_BLOCK_SELECTOR = 'body'; +export const ROOT_BLOCK_SUPPORTS = [ + 'background', + 'backgroundColor', + 'color', + 'linkColor', + 'buttonColor', + 'fontFamily', + 'fontSize', + 'fontStyle', + 'fontWeight', + 'lineHeight', + 'textDecoration', + 'textTransform', + 'padding', +]; + +export const PRESET_METADATA = [ + { + path: [ 'color', 'palette' ], + valueKey: 'color', + cssVarInfix: 'color', + classes: [ + { classSuffix: 'color', propertyName: 'color' }, + { + classSuffix: 'background-color', + propertyName: 'background-color', + }, + { + classSuffix: 'border-color', + propertyName: 'border-color', + }, + ], + }, + { + path: [ 'color', 'gradients' ], + valueKey: 'gradient', + cssVarInfix: 'gradient', + classes: [ + { + classSuffix: 'gradient-background', + propertyName: 'background', + }, + ], + }, + { + path: [ 'color', 'duotone' ], + cssVarInfix: 'duotone', + valueFunc: ( { slug } ) => `url( '#wp-duotone-${ slug }' )`, + classes: [], + }, + { + path: [ 'typography', 'fontSizes' ], + valueFunc: ( preset, { typography: typographySettings } ) => + getTypographyFontSizeValue( preset, typographySettings ), + valueKey: 'size', + cssVarInfix: 'font-size', + classes: [ { classSuffix: 'font-size', propertyName: 'font-size' } ], + }, + { + path: [ 'typography', 'fontFamilies' ], + valueKey: 'fontFamily', + cssVarInfix: 'font-family', + classes: [ + { classSuffix: 'font-family', propertyName: 'font-family' }, + ], + }, + { + path: [ 'spacing', 'spacingSizes' ], + valueKey: 'size', + cssVarInfix: 'spacing', + valueFunc: ( { size } ) => size, + classes: [], + }, +]; + +export const STYLE_PATH_TO_CSS_VAR_INFIX = { + 'color.background': 'color', + 'color.text': 'color', + 'elements.link.color.text': 'color', + 'elements.button.color.text': 'color', + 'elements.button.backgroundColor': 'background-color', + 'elements.heading.color': 'color', + 'elements.heading.backgroundColor': 'background-color', + 'elements.heading.gradient': 'gradient', + 'color.gradient': 'gradient', + 'typography.fontSize': 'font-size', + 'typography.fontFamily': 'font-family', +}; + +// A static list of block attributes that store global style preset slugs. +export const STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE = { + 'color.background': 'backgroundColor', + 'color.text': 'textColor', + 'color.gradient': 'gradient', + 'typography.fontSize': 'fontSize', + 'typography.fontFamily': 'fontFamily', +}; + +function findInPresetsBy( + features, + blockName, + presetPath, + presetProperty, + presetValueValue +) { + // Block presets take priority above root level presets. + const orderedPresetsByOrigin = [ + get( features, [ 'blocks', blockName, ...presetPath ] ), + get( features, presetPath ), + ]; + + for ( const presetByOrigin of orderedPresetsByOrigin ) { + if ( presetByOrigin ) { + // Preset origins ordered by priority. + const origins = [ 'custom', 'theme', 'default' ]; + for ( const origin of origins ) { + const presets = presetByOrigin[ origin ]; + if ( presets ) { + const presetObject = presets.find( + ( preset ) => + preset[ presetProperty ] === presetValueValue + ); + if ( presetObject ) { + if ( presetProperty === 'slug' ) { + return presetObject; + } + // If there is a highest priority preset with the same slug but different value the preset we found was overwritten and should be ignored. + const highestPresetObjectWithSameSlug = findInPresetsBy( + features, + blockName, + presetPath, + 'slug', + presetObject.slug + ); + if ( + highestPresetObjectWithSameSlug[ + presetProperty + ] === presetObject[ presetProperty ] + ) { + return presetObject; + } + return undefined; + } + } + } + } + } +} + +export function getPresetVariableFromValue( + features, + blockName, + variableStylePath, + presetPropertyValue +) { + if ( ! presetPropertyValue ) { + return presetPropertyValue; + } + + const cssVarInfix = STYLE_PATH_TO_CSS_VAR_INFIX[ variableStylePath ]; + + const metadata = PRESET_METADATA.find( + ( data ) => data.cssVarInfix === cssVarInfix + ); + + if ( ! metadata ) { + // The property doesn't have preset data + // so the value should be returned as it is. + return presetPropertyValue; + } + const { valueKey, path } = metadata; + + const presetObject = findInPresetsBy( + features, + blockName, + path, + valueKey, + presetPropertyValue + ); + + if ( ! presetObject ) { + // Value wasn't found in the presets, + // so it must be a custom value. + return presetPropertyValue; + } + + return `var:preset|${ cssVarInfix }|${ presetObject.slug }`; +} + +function getValueFromPresetVariable( + features, + blockName, + variable, + [ presetType, slug ] +) { + const metadata = PRESET_METADATA.find( + ( data ) => data.cssVarInfix === presetType + ); + if ( ! metadata ) { + return variable; + } + + const presetObject = findInPresetsBy( + features.settings, + blockName, + metadata.path, + 'slug', + slug + ); + + if ( presetObject ) { + const { valueKey } = metadata; + const result = presetObject[ valueKey ]; + return getValueFromVariable( features, blockName, result ); + } + + return variable; +} + +function getValueFromCustomVariable( features, blockName, variable, path ) { + const result = + get( features.settings, [ 'blocks', blockName, 'custom', ...path ] ) ?? + get( features.settings, [ 'custom', ...path ] ); + if ( ! result ) { + return variable; + } + // A variable may reference another variable so we need recursion until we find the value. + return getValueFromVariable( features, blockName, result ); +} + +/** + * Attempts to fetch the value of a theme.json CSS variable. + * + * @param {Object} features GlobalStylesContext config, e.g., user, base or merged. Represents the theme.json tree. + * @param {string} blockName The name of a block as represented in the styles property. E.g., 'root' for root-level, and 'core/${blockName}' for blocks. + * @param {string|*} variable An incoming style value. A CSS var value is expected, but it could be any value. + * @return {string|*|{ref}} The value of the CSS var, if found. If not found, the passed variable argument. + */ +export function getValueFromVariable( features, blockName, variable ) { + if ( ! variable || typeof variable !== 'string' ) { + if ( variable?.ref && typeof variable?.ref === 'string' ) { + const refPath = variable.ref.split( '.' ); + variable = get( features, refPath ); + // Presence of another ref indicates a reference to another dynamic value. + // Pointing to another dynamic value is not supported. + if ( ! variable || !! variable?.ref ) { + return variable; + } + } else { + return variable; + } + } + const USER_VALUE_PREFIX = 'var:'; + const THEME_VALUE_PREFIX = 'var(--wp--'; + const THEME_VALUE_SUFFIX = ')'; + + let parsedVar; + + if ( variable.startsWith( USER_VALUE_PREFIX ) ) { + parsedVar = variable.slice( USER_VALUE_PREFIX.length ).split( '|' ); + } else if ( + variable.startsWith( THEME_VALUE_PREFIX ) && + variable.endsWith( THEME_VALUE_SUFFIX ) + ) { + parsedVar = variable + .slice( THEME_VALUE_PREFIX.length, -THEME_VALUE_SUFFIX.length ) + .split( '--' ); + } else { + // We don't know how to parse the value: either is raw of uses complex CSS such as `calc(1px * var(--wp--variable) )` + return variable; + } + + const [ type, ...path ] = parsedVar; + if ( type === 'preset' ) { + return getValueFromPresetVariable( + features, + blockName, + variable, + path + ); + } + if ( type === 'custom' ) { + return getValueFromCustomVariable( + features, + blockName, + variable, + path + ); + } + return variable; +} + +/** + * Function that scopes a selector with another one. This works a bit like + * SCSS nesting except the `&` operator isn't supported. + * + * @example + * ```js + * const scope = '.a, .b .c'; + * const selector = '> .x, .y'; + * const merged = scopeSelector( scope, selector ); + * // merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' + * ``` + * + * @param {string} scope Selector to scope to. + * @param {string} selector Original selector. + * + * @return {string} Scoped selector. + */ +export function scopeSelector( scope, selector ) { + const scopes = scope.split( ',' ); + const selectors = selector.split( ',' ); + + const selectorsScoped = []; + scopes.forEach( ( outer ) => { + selectors.forEach( ( inner ) => { + selectorsScoped.push( `${ outer.trim() } ${ inner.trim() }` ); + } ); + } ); + + return selectorsScoped.join( ', ' ); +} diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 70ab1ac333c44a..efb0b3570b959a 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -141,6 +141,7 @@ function useBlockEditorSettings( settings, hasTemplate ) { '__experimentalPreferredStyleVariations', '__experimentalSetIsInserterOpened', '__unstableGalleryWithImageBlocks', + '__experimentalStyles', 'alignWide', 'allowedBlockTypes', 'bodyPlaceholder',