-
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
Site Editor: Add new 'Push changes to Global Styles' button #46446
Changes from all commits
b71ba6f
88769a4
e253915
277d3e5
3f4bea1
1ed8b2b
fda09e8
d587847
44ad315
824353d
44d0345
8a80df4
939b7cd
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,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 ) ); | ||
} | ||
|
||
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' ) } | ||
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. Not a showstopper, just asking: Is "Global Styles" a term that we use in the editor? Would something like 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. You're right, we don't call it Global Styles anywhere in the UI. How about "Apply styles to all Heading blocks"? (or "Move"?) 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. 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. Hmmm it's so long, looks a bit ridiculous 😅 Other options: Push changes to Styles (technically this is what we call Global Styles) 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 we make @jasmussen and @jameskoster decide? 😛 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.
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%; | ||
} |
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' ); | ||
} ); | ||
} ); |
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 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.
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.
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 usesetImmutably()
which already clones the object anyway, so there may be no need to clone it additionally.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 had a feeling you already had a plan for this :)
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.
Sure thing! Besides #52278 and #52279 it's the last unaddressed usage of
_.set()
!