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

Site Editor: Add new 'Push changes to Global Styles' button #46446

Merged
merged 13 commits into from
Dec 16, 2022
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function useGlobalStylesUserConfig() {
}, [ settings, styles ] );

const setConfig = useCallback(
( callback ) => {
( callback, options = {} ) => {
const record = getEditedEntityRecord(
'root',
'globalStyles',
Expand All @@ -105,10 +105,16 @@ function useGlobalStylesUserConfig() {
settings: record?.settings ?? {},
};
const updatedConfig = callback( currentConfig );
editEntityRecord( 'root', 'globalStyles', globalStylesId, {
styles: cleanEmptyObject( updatedConfig.styles ) || {},
settings: cleanEmptyObject( updatedConfig.settings ) || {},
} );
editEntityRecord(
'root',
'globalStyles',
globalStylesId,
{
styles: cleanEmptyObject( updatedConfig.styles ) || {},
settings: cleanEmptyObject( updatedConfig.settings ) || {},
},
options
);
},
[ globalStylesId ]
);
Expand Down
11 changes: 10 additions & 1 deletion packages/edit-site/src/components/global-styles/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export const PRESET_METADATA = [
},
];

const STYLE_PATH_TO_CSS_VAR_INFIX = {
export const STYLE_PATH_TO_CSS_VAR_INFIX = {
'color.background': 'color',
'color.text': 'color',
'elements.link.color.text': 'color',
Expand All @@ -100,6 +100,15 @@ const STYLE_PATH_TO_CSS_VAR_INFIX = {
'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,
Expand Down
1 change: 1 addition & 0 deletions packages/edit-site/src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
* Internal dependencies
*/
import './components';
import './push-changes-to-global-styles';
import './template-part-edit';
162 changes: 162 additions & 0 deletions packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* External dependencies
*/
import { get, set } from 'lodash';

/**
* WordPress dependencies
*/
import { addFilter } from '@wordpress/hooks';
import { createHigherOrderComponent } from '@wordpress/compose';
import {
InspectorAdvancedControls,
store as blockEditorStore,
} from '@wordpress/block-editor';
import { BaseControl, Button } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import {
__EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY,
getBlockType,
} from '@wordpress/blocks';
import { useContext, useMemo, useCallback } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import { getSupportedGlobalStylesPanels } from '../../components/global-styles/hooks';
import { GlobalStylesContext } from '../../components/global-styles/context';
import {
STYLE_PATH_TO_CSS_VAR_INFIX,
STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE,
} from '../../components/global-styles/utils';

function getChangesToPush( name, attributes ) {
return getSupportedGlobalStylesPanels( name ).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 }`
: get( attributes.style, path );
return value ? [ { path, value } ] : [];
} );
}

function cloneDeep( object ) {
return ! object ? {} : JSON.parse( JSON.stringify( object ) );
}
Comment on lines +53 to +55
Copy link
Member

Choose a reason for hiding this comment

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

I came across this code while testing a different PR.

@tyxla, if I recall correctly, we already have a similar utility method. Maybe we should use it here as well.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the reminder @Mamaduka!

This is going to be addressed soon as part of the Lodash removal. Instead of the set() usages I'm planning to use setImmutably() which already clones the object anyway, so there may be no need to clone it additionally.

Copy link
Member

Choose a reason for hiding this comment

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

I had a feeling you already had a plan for this :)

Copy link
Member

Choose a reason for hiding this comment

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

Sure thing! Besides #52278 and #52279 it's the last unaddressed usage of _.set()!


function PushChangesToGlobalStylesControl( {
name,
attributes,
setAttributes,
} ) {
const changes = useMemo(
() => getChangesToPush( name, attributes ),
[ name, attributes ]
);

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

const { __unstableMarkNextChangeAsNotPersistent } =
useDispatch( blockEditorStore );
const { createSuccessNotice } = useDispatch( noticesStore );

const pushChanges = useCallback( () => {
if ( changes.length === 0 ) {
return;
}

const { style: blockStyles } = attributes;

const newBlockStyles = cloneDeep( blockStyles );
const newUserConfig = cloneDeep( userConfig );

for ( const { path, value } of changes ) {
set( newBlockStyles, path, undefined );
set( newUserConfig, [ 'styles', 'blocks', name, ...path ], value );
}

// @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 } );
setUserConfig( () => newUserConfig, { undoIgnore: true } );

createSuccessNotice(
sprintf(
// translators: %s: Title of the block e.g. 'Heading'.
__( 'Pushed styles to all %s blocks.' ),
getBlockType( name ).title
),
{
type: 'snackbar',
actions: [
{
label: __( 'Undo' ),
onClick() {
__unstableMarkNextChangeAsNotPersistent();
setAttributes( { style: blockStyles } );
setUserConfig( () => userConfig, {
undoIgnore: true,
} );
},
},
],
}
);
}, [ changes, attributes, userConfig, name ] );

return (
<BaseControl
className="edit-site-push-changes-to-global-styles-control"
help={ sprintf(
// translators: %s: Title of the block e.g. 'Heading'.
__(
'Move this block’s typography, spacing, dimensions, and color styles to all %s blocks.'
),
getBlockType( name ).title
) }
>
<BaseControl.VisualLabel>
{ __( 'Styles' ) }
</BaseControl.VisualLabel>
<Button
variant="primary"
disabled={ changes.length === 0 }
onClick={ pushChanges }
>
{ __( 'Push changes to Global Styles' ) }
Copy link
Member Author

Choose a reason for hiding this comment

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

Not a showstopper, just asking:

Is "Global Styles" a term that we use in the editor? Would something like Apply styles globally communicate what we want to say?

Copy link
Member

Choose a reason for hiding this comment

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

You're right, we don't call it Global Styles anywhere in the UI. How about "Apply styles to all Heading blocks"? (or "Move"?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds grand.

I tried it out and we might need to adjust white space for some block names (taking into account i18n as well)

Screen Shot 2022-12-16 at 12 56 25 pm

The following has, for example:

    white-space: normal;
    padding: $grid-unit-10;
    height: auto;

Screen Shot 2022-12-16 at 12 58 16 pm

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm it's so long, looks a bit ridiculous 😅

Other options:

Push changes to Styles (technically this is what we call Global Styles)
Push styles
Apply styles globally

Copy link
Member

Choose a reason for hiding this comment

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

How about we make @jasmussen and @jameskoster decide? 😛

Copy link
Member Author

Choose a reason for hiding this comment

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

How about we make @jasmussen and @jameskoster decide? 😛

Delegate. I like it.

</Button>
</BaseControl>
);
}

const withPushChangesToGlobalStyles = createHigherOrderComponent(
( BlockEdit ) => ( props ) =>
(
<>
<BlockEdit { ...props } />
<InspectorAdvancedControls>
<PushChangesToGlobalStylesControl { ...props } />
</InspectorAdvancedControls>
</>
)
);

addFilter(
'editor.BlockEdit',
'core/edit-site/push-changes-to-global-styles',
withPushChangesToGlobalStyles
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.edit-site-push-changes-to-global-styles-control .components-button {
justify-content: center;
width: 100%;
}
1 change: 1 addition & 0 deletions packages/edit-site/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
@import "./components/sidebar-navigation-title/style.scss";
@import "./components/site-icon/style.scss";
@import "./components/style-book/style.scss";
@import "./hooks/push-changes-to-global-styles/style.scss";

html #wpadminbar {
display: none;
Expand Down
109 changes: 109 additions & 0 deletions test/e2e/specs/site-editor/push-to-global-styles.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* WordPress dependencies
*/
const {
test,
expect,
Editor,
} = require( '@wordpress/e2e-test-utils-playwright' );

test.use( {
editor: async ( { page }, use ) => {
await use( new Editor( { page, hasIframe: true } ) );
},
} );

test.describe( 'Push to Global Styles button', () => {
noisysocks marked this conversation as resolved.
Show resolved Hide resolved
test.beforeAll( async ( { requestUtils } ) => {
await Promise.all( [
requestUtils.activateTheme( 'emptytheme' ),
requestUtils.deleteAllTemplates( 'wp_template' ),
requestUtils.deleteAllTemplates( 'wp_template_part' ),
] );
} );

test.afterAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'twentytwentyone' );
} );

test.beforeEach( async ( { admin, siteEditor } ) => {
await admin.visitSiteEditor();
await siteEditor.enterEditMode();
} );

test( 'should apply Heading block styles to all Heading blocks', async ( {
page,
editor,
} ) => {
// Add a Heading block.
await editor.insertBlock( { name: 'core/heading' } );
await page.keyboard.type( 'A heading' );

// Navigate to Styles -> Blocks -> Heading -> Typography
await page.getByRole( 'button', { name: 'Styles' } ).click();
await page.getByRole( 'button', { name: 'Blocks styles' } ).click();
await page
.getByRole( 'button', { name: 'Heading block styles' } )
.click();
await page.getByRole( 'button', { name: 'Typography styles' } ).click();

// Headings should not have uppercase
await expect(
page.getByRole( 'button', { name: 'Uppercase' } )
).toHaveAttribute( 'aria-pressed', 'false' );

// Go to block settings and open the Advanced panel
await page.getByRole( 'button', { name: 'Settings' } ).click();
await page.getByRole( 'button', { name: 'Advanced' } ).click();

// Push button should be disabled
await expect(
page.getByRole( 'button', {
name: 'Push changes to Global Styles',
} )
).toBeDisabled();

// Make the Heading block uppercase
await page.getByRole( 'button', { name: 'Uppercase' } ).click();

// Push button should now be enabled
await expect(
page.getByRole( 'button', {
name: 'Push changes to Global Styles',
} )
).toBeEnabled();

// Press the Push button
await page
.getByRole( 'button', { name: 'Push changes to Global Styles' } )
.click();

// Snackbar notification should appear
await expect(
page.getByRole( 'button', {
name: 'Dismiss this notice',
text: 'Pushed styles to all Heading blocks.',
} )
).toBeVisible();

// Push button should be disabled again
await expect(
page.getByRole( 'button', {
name: 'Push changes to Global Styles',
} )
).toBeDisabled();

// Navigate again to Styles -> Blocks -> Heading -> Typography
await page.getByRole( 'button', { name: 'Styles' } ).click();
await page.getByRole( 'button', { name: 'Blocks styles' } ).click();
await page
.getByRole( 'button', { name: 'Heading block styles' } )
.click();
await page.getByRole( 'button', { name: 'Typography styles' } ).click();

// Headings should now have uppercase
await expect(
page.getByRole( 'button', { name: 'Uppercase' } )
).toHaveAttribute( 'aria-pressed', 'true' );
} );
} );