-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Add: API to allow blocks to access global styles. #34178
Changes from all commits
be5028c
c866a26
5cdb0f4
6773622
f23ad12
d926d64
b1a1103
c78a322
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ] ); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
export { default as transformStyles } from './transform-styles'; | ||
export * from './theme'; | ||
export * from './block-variation-transforms'; | ||
export { | ||
getValueFromVariable as __experimentalGetValueFromVariable, | ||
getPresetVariableFromValue as __experimentalGetPresetVariableFromValue, | ||
} from './style-variable-resolution'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { get, find, isString, kebabCase } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { __EXPERIMENTAL_PRESET_METADATA as PRESET_METADATA } from '@wordpress/blocks'; | ||
|
||
const STYLE_PROPERTIES_TO_CSS_VAR_INFIX = { | ||
linkColor: 'color', | ||
backgroundColor: 'color', | ||
background: 'gradient', | ||
}; | ||
|
||
function findInPresetsBy( | ||
features, | ||
blockName, | ||
presetPath, | ||
presetProperty, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about naming these two There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems the role of The complexity here is that it needs to take into account that 1) there may be a block and a global preset and 2) any of those may have core, theme, and user presets. This is the full list of candidate presets available, sorted by priority:
My understanding is that we should return the first candidate whose There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated in
And it works as described at #33149 Code, for reference: function findInPresetsBy(
styles,
blockName,
presetPath,
presetPropertyName,
presetPropertyValue
) {
const candidates = [];
const origins = [ 'user', 'theme', 'user' ];
const blockPresets = get( styles, [
'settings',
'blocks',
blockName,
...presetPath,
] );
origins.forEach( ( origin ) => {
if ( blockPresets?.[ origin ] ) {
blockPresets[ origin ].forEach( ( preset ) => {
if ( preset[ presetPropertyName ] === presetPropertyValue ) {
candidates.push( preset );
}
} );
}
} );
const globalPresets = get( styles, [ 'settings', ...presetPath ] );
origins.forEach( ( origin ) => {
if ( globalPresets?.[ origin ] ) {
globalPresets[ origin ].forEach( ( preset ) => {
if ( preset[ presetPropertyName ] === presetPropertyValue ) {
candidates.push( preset );
}
} );
}
} );
return candidates.length >= 1 ? candidates[ 0 ] : undefined;
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @oandregal,
I don't think it is enough.
And theme defines:
If we search the color property value of "#1" without the recursion we would return the theme preset, but we should not return it because the variable was overwritten and now there is no variable with color "#1". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice one 👍 I've pushed some tests in c78a322 to make sure this behavior is documented and retained. |
||
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 = [ 'user', 'theme', 'core' ]; | ||
for ( const origin of origins ) { | ||
const presets = presetByOrigin[ origin ]; | ||
if ( presets ) { | ||
const presetObject = find( | ||
presets, | ||
( 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; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
function getValueFromPresetVariable( | ||
features, | ||
blockName, | ||
variable, | ||
[ presetType, slug ] | ||
) { | ||
const metadata = find( PRESET_METADATA, [ 'cssVarInfix', presetType ] ); | ||
if ( ! metadata ) { | ||
return variable; | ||
} | ||
|
||
const presetObject = findInPresetsBy( | ||
features, | ||
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, [ 'blocks', blockName, 'custom', ...path ] ) ?? | ||
get( features, [ '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 ); | ||
} | ||
|
||
export function getValueFromVariable( features, blockName, variable ) { | ||
if ( ! variable || ! isString( variable ) ) { | ||
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following up on my comment, is the intention that something like If so, maybe we could return Not sure if that'll affect anything else 😄 , but doing this locally prevents the Or I guess we could handle it in the line-height hook itself, by checking for a number-type value. That might get more complex if we ever introduce units to line-height values though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also could only reproduce the issue when the custom variable isn't actually defined under the I tested out returning
That tested well for me, including with the recursion with custom variables pointing to other custom variables etc. |
||
features, | ||
blockName, | ||
variable, | ||
path | ||
); | ||
} | ||
return variable; | ||
} | ||
|
||
export function getPresetVariableFromValue( | ||
features, | ||
blockName, | ||
presetPropertyName, | ||
presetPropertyValue | ||
) { | ||
if ( ! presetPropertyValue ) { | ||
return presetPropertyValue; | ||
} | ||
|
||
const cssVarInfix = | ||
STYLE_PROPERTIES_TO_CSS_VAR_INFIX[ presetPropertyName ] || | ||
kebabCase( presetPropertyName ); | ||
|
||
const metadata = find( PRESET_METADATA, [ '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 }`; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this way of doing things. ❤️ Having the default value as the placeholder works I think as I can still set numerical values to
0
for example, and the native appearance (greyed out) indicates that it's a default value.Just noting a couple of things down, all of which are probably best for follow ups if this gets off the ground.
px
,vh
etc) from the default values and reset them in the controls where units change<UnitControl />
directly such as border-width should just work 👍There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point regarding the units @ramonjd. I guess we can deal with units a follow-up to avoid growing this PR.