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: Fix push-to-global-styles clearing of attributes, border fallbacks, link hover colors, and behaviors #51621

Merged
merged 7 commits into from
Aug 11, 2023
189 changes: 162 additions & 27 deletions packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { BaseControl, Button } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import {
__EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY,
__EXPERIMENTAL_STYLE_PROPERTY,
getBlockType,
hasBlockSupport,
} from '@wordpress/blocks';
Expand All @@ -27,15 +27,25 @@ import { useSupportedStyles } from '../../components/global-styles/hooks';
import { unlock } from '../../lock-unlock';

const {
cleanEmptyObject,
GlobalStylesContext,
__experimentalUseGlobalBehaviors: useGlobalBehaviors,
__experimentalUseHasBehaviorsPanel: useHasBehaviorsPanel,
} = unlock( blockEditorPrivateApis );

// Block Gap is a special case and isn't defined within the blocks
// style properties config. We'll add it here to allow it to be pushed
// to global styles as well.
const STYLE_PROPERTY = {
...__EXPERIMENTAL_STYLE_PROPERTY,
blockGap: { value: [ 'spacing', 'blockGap' ] },
};

// TODO: Temporary duplication of constant in @wordpress/block-editor. Can be
// removed by moving PushChangesToGlobalStylesControl to
// @wordpress/block-editor.
const STYLE_PATH_TO_CSS_VAR_INFIX = {
'border.color': 'color',
'color.background': 'color',
'color.text': 'color',
'elements.link.color.text': 'color',
Expand Down Expand Up @@ -77,6 +87,7 @@ const STYLE_PATH_TO_CSS_VAR_INFIX = {
'elements.h6.typography.fontFamily': 'font-family',
'elements.h6.color.gradient': 'gradient',
'color.gradient': 'gradient',
blockGap: 'spacing',
'typography.fontSize': 'font-size',
'typography.fontFamily': 'font-family',
};
Expand All @@ -85,6 +96,7 @@ const STYLE_PATH_TO_CSS_VAR_INFIX = {
// removed by moving PushChangesToGlobalStylesControl to
// @wordpress/block-editor.
const STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE = {
'border.color': 'borderColor',
'color.background': 'backgroundColor',
'color.text': 'textColor',
'color.gradient': 'gradient',
Expand All @@ -102,30 +114,127 @@ const getValueFromObjectPath = ( object, path ) => {
return value;
};

function useChangesToPush( name, attributes ) {
const flatBorderProperties = [ 'borderColor', 'borderWidth', 'borderStyle' ];
const sides = [ 'top', 'right', 'bottom', 'left' ];

function getBorderStyleChanges( border, presetColor, userStyle ) {
if ( ! border && ! presetColor ) {
return [];
}

const changes = [
...getFallbackBorderStyleChange( 'top', border, userStyle ),
...getFallbackBorderStyleChange( 'right', border, userStyle ),
...getFallbackBorderStyleChange( 'bottom', border, userStyle ),
...getFallbackBorderStyleChange( 'left', border, userStyle ),
];

// Handle a flat border i.e. all sides the same, CSS shorthand.
const { color: customColor, style, width } = border || {};
const hasColorOrWidth = presetColor || customColor || width;

if ( hasColorOrWidth && ! style ) {
// Global Styles need individual side configurations to overcome
// theme.json configurations which are per side as well.
sides.forEach( ( side ) => {
// Only add fallback border-style if global styles don't already
// have something set.
if ( ! userStyle?.[ side ]?.style ) {
changes.push( {
path: [ 'border', side, 'style' ],
value: 'solid',
} );
}
} );
}

return changes;
}

function getFallbackBorderStyleChange( side, border, globalBorderStyle ) {
if ( ! border?.[ side ] || globalBorderStyle?.[ side ]?.style ) {
return [];
}

const { color, style, width } = border[ side ];
const hasColorOrWidth = color || width;

if ( ! hasColorOrWidth || style ) {
return [];
}

return [ { path: [ 'border', side, 'style' ], value: 'solid' } ];
}

function useChangesToPush( name, attributes, userConfig ) {
const supports = useSupportedStyles( name );
const blockUserConfig = userConfig?.styles?.blocks?.[ name ];

return useMemo(
() =>
supports.flatMap( ( key ) => {
if ( ! STYLE_PROPERTY[ key ] ) {
return [];
return useMemo( () => {
const changes = supports.flatMap( ( key ) => {
if ( ! STYLE_PROPERTY[ key ] ) {
return [];
}
const { value: path } = STYLE_PROPERTY[ key ];
const presetAttributeKey = path.join( '.' );
const presetAttributeValue =
attributes[
STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE[ presetAttributeKey ]
];
const value = presetAttributeValue
? `var:preset|${ STYLE_PATH_TO_CSS_VAR_INFIX[ presetAttributeKey ] }|${ presetAttributeValue }`
: getValueFromObjectPath( attributes.style, path );

// Links only have a single support entry but have two element
// style properties, color and hover color. The following check
// will add the hover color to the changes if required.
if ( key === 'linkColor' ) {
const linkChanges = value ? [ { path, value } ] : [];
const hoverPath = [
'elements',
'link',
':hover',
'color',
'text',
];
const hoverValue = getValueFromObjectPath(
attributes.style,
hoverPath
);

if ( hoverValue ) {
linkChanges.push( { path: hoverPath, value: hoverValue } );
}
const { value: path } = STYLE_PROPERTY[ key ];
const presetAttributeKey = path.join( '.' );
const presetAttributeValue =
attributes[
STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE[
presetAttributeKey
]
];
const value = presetAttributeValue
? `var:preset|${ STYLE_PATH_TO_CSS_VAR_INFIX[ presetAttributeKey ] }|${ presetAttributeValue }`
: getValueFromObjectPath( attributes.style, path );
return value ? [ { path, value } ] : [];
} ),
[ supports, attributes ]
);

return linkChanges;
}

// The shorthand border styles can't be mapped directly as global
// styles requires longhand config.
if ( flatBorderProperties.includes( key ) && value ) {
// The shorthand config path is included to clear the block attribute.
const borderChanges = [ { path, value } ];
sides.forEach( ( side ) => {
const currentPath = [ ...path ];
currentPath.splice( -1, 0, side );
borderChanges.push( { path: currentPath, value } );
} );
return borderChanges;
}
Comment on lines +212 to +223
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's an issue where if a theme.json configures longhand border properties and the user's global styles only configured shorthand border styles, the theme.json styles would incorrectly take precedence. As a result, Global Styles applies even shorthand borders to the longhand border properties.

This block of code maps shorthand block instance border styles to the longhand paths required by global styles.


return value ? [ { path, value } ] : [];
} );

// To ensure display of a visible border, global styles require a
// default border style if a border color or width is present.
getBorderStyleChanges(
attributes.style?.border,
attributes.borderColor,
blockUserConfig?.border
).forEach( ( change ) => changes.push( change ) );

return changes;
}, [ supports, attributes, blockUserConfig ] );
}

/**
Expand Down Expand Up @@ -177,14 +286,15 @@ function PushChangesToGlobalStylesControl( {
attributes,
setAttributes,
} ) {
const changes = useChangesToPush( name, attributes );

const hasBehaviorsPanel = useHasBehaviorsPanel( attributes, name, {
blockSupportOnly: true,
} );

const { user: userConfig, setUserConfig } =
useContext( GlobalStylesContext );

const changes = useChangesToPush( name, attributes, userConfig );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We now pass the current user global styles to assist with determining if we need to provide a fallback border style to maintain a visible border.


const { __unstableMarkNextChangeAsNotPersistent } =
useDispatch( blockEditorStore );
const { createSuccessNotice } = useDispatch( noticesStore );
Expand All @@ -198,6 +308,7 @@ function PushChangesToGlobalStylesControl( {
if ( changes.length === 0 && ! userHasEditedBehaviors ) {
return;
}

if ( changes.length > 0 ) {
const { style: blockStyles } = attributes;

Expand All @@ -213,12 +324,22 @@ function PushChangesToGlobalStylesControl( {
);
}

const newBlockAttributes = {
borderColor: undefined,
backgroundColor: undefined,
textColor: undefined,
gradient: undefined,
fontSize: undefined,
fontFamily: undefined,
style: cleanEmptyObject( newBlockStyles ),
};

// @wordpress/core-data doesn't support editing multiple entity types in
// a single undo level. So for now, we disable @wordpress/core-data undo
// tracking and implement our own Undo button in the snackbar
// notification.
__unstableMarkNextChangeAsNotPersistent();
setAttributes( { style: newBlockStyles } );
setAttributes( newBlockAttributes );
setUserConfig( () => newUserConfig, { undoIgnore: true } );
createSuccessNotice(
sprintf(
Expand All @@ -233,7 +354,7 @@ function PushChangesToGlobalStylesControl( {
label: __( 'Undo' ),
onClick() {
__unstableMarkNextChangeAsNotPersistent();
setAttributes( { style: blockStyles } );
setAttributes( attributes );
setUserConfig( () => userConfig, {
undoIgnore: true,
} );
Expand All @@ -243,8 +364,10 @@ function PushChangesToGlobalStylesControl( {
}
);
}

if ( userHasEditedBehaviors ) {
__unstableMarkNextChangeAsNotPersistent();
setAttributes( { behaviors: undefined } );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A good potential follow-up here might be to consolidate the updating of attributes and snackbar notices for both styles and behaviours. To keep the scope of this PR manageable for testing I've opted against this larger refactor.

setBehavior( attributes.behaviors );
createSuccessNotice(
sprintf(
Expand All @@ -269,7 +392,19 @@ function PushChangesToGlobalStylesControl( {
}
);
}
}, [ changes, attributes, userConfig, name ] );
}, [
__unstableMarkNextChangeAsNotPersistent,
attributes,
changes,
createSuccessNotice,
inheritedBehaviors,
name,
setAttributes,
setBehavior,
setUserConfig,
userConfig,
userHasEditedBehaviors,
] );

return (
<BaseControl
Expand Down
Loading