Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Global Styles: summarize what changed on each block in the block list
Replace the indicator dot with a line naming the styles the user changed,
e.g. "Changed: Colors, Typography, Plain variation". The dot said a block
had been touched but not what was touched, so it raised the question it
was meant to answer.

Reuse getGlobalStylesChangelist for the categories, calling it per block
so the reported paths start at the block's own entry rather than at the
whole config, where the block name consumes both levels the changelist
keeps. Name a changed style variation rather than describing it, since
the categories inside it are rarely what the reader is looking for.

The changelist only reports background, color, typography and spacing, so
a block changed only through its border counts as customized but has no
summary to show. Left as is, to be addressed separately.
  • Loading branch information
ramonjd committed Aug 18, 2026
commit 1da774ed89b794229fd303174d0bb7378796110e
4 changes: 4 additions & 0 deletions packages/global-styles-engine/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Enhancements

- Export `getGlobalStylesChangelist`, which returns the changed styles as translated labels grouped by type ([#81373](https://github.com/WordPress/gutenberg/pull/81373)).

## 1.20.0 (2026-08-12)

### Bug Fixes
Expand Down
5 changes: 4 additions & 1 deletion packages/global-styles-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export { default as getPalettes } from './settings/get-palette';
// Utilities
export { areGlobalStylesEqual } from './core/equal';
export { mergeGlobalStyles } from './core/merge';
export { default as getGlobalStylesChanges } from './utils/get-global-styles-changes';
export {
default as getGlobalStylesChanges,
getGlobalStylesChangelist,
} from './utils/get-global-styles-changes';

// Core rendering
export { generateGlobalStyles } from './core/render';
Expand Down
2 changes: 1 addition & 1 deletion packages/global-styles-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Enhancements

- Expose typography and color controls for citations, inputs, and selects in Global Styles ([#80852](https://github.com/WordPress/gutenberg/pull/80852)).
- Summarize the styles a user changed on each block in the block list, and add a filter to show only the blocks they have changed ([#81373](https://github.com/WordPress/gutenberg/pull/81373)).

## 1.20.0 (2026-08-12)

Expand All @@ -15,7 +16,6 @@
### Enhancements

- Add a `showBlockStateControls` prop to `GlobalStylesUI`, defaulting to `true`, which hides the state controls for blocks when set to `false` ([#80956](https://github.com/WordPress/gutenberg/pull/80956)).
- Mark blocks that have user styles in the block list, and add a filter to show only those blocks ([#81316](https://github.com/WordPress/gutenberg/issues/81316)).

## 1.19.0 (2026-07-29)

Expand Down
146 changes: 116 additions & 30 deletions packages/global-styles-ui/src/screen-block-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
__experimentalHStack as HStack,
__experimentalText as WCText,
} from '@wordpress/components';
import { VisuallyHidden } from '@wordpress/ui';
import { useSelect } from '@wordpress/data';
import {
useState,
Expand All @@ -19,7 +18,10 @@ import {
useDeferredValue,
memo,
} from '@wordpress/element';
import type { GlobalStylesConfig } from '@wordpress/global-styles-engine';
import {
getGlobalStylesChangelist,
type GlobalStylesConfig,
} from '@wordpress/global-styles-engine';
import {
BlockIcon,
privateApis as blockEditorPrivateApis,
Expand Down Expand Up @@ -95,6 +97,79 @@ export function hasUserStylesForBlock(
);
}

interface BlockStyle {
name: string;
label?: string;
}

/**
* A short summary of what the user changed on a block, e.g.
* "Changed: Typography, Colors, Plain variation".
*
* A changed style variation is named rather than described: whatever was
* changed inside it, it contributes one "{Label} variation" entry.
*
* Returns an empty string when nothing can be named. `getGlobalStylesChangelist`
* only reports background, color, typography and spacing, so a block changed
* only through, say, its border counts as customized but has nothing to show.
*
* @param user The user's global styles config.
* @param blockName The block to summarize, e.g. `core/quote`.
* @param registeredStyles The block's registered style variations, used to turn
* a variation's slug into its label.
* @return The summary, or an empty string.
*/
export function getUserStylesSummary(
user: GlobalStylesConfig | undefined,
blockName: string,
registeredStyles: BlockStyle[] = []
): string {
const blockStyles = user?.styles?.blocks?.[ blockName ];
// Compare this block's own entry against nothing, so the changelist reports
// the block's categories rather than the whole config's. `variations` is not
// one of the keys it inspects, so it never reports them here.
const changes = getGlobalStylesChangelist(
{
styles: blockStyles,
settings: user?.settings?.blocks?.[ blockName ],
},
{}
);
const entries = [ ...new Set( changes.map( ( [ , label ] ) => label ) ) ];

// Name each changed variation instead of describing what changed in it.
Object.entries( blockStyles?.variations ?? {} ).forEach(
( [ slug, variationStyles ] ) => {
if ( ! hasAnyValue( variationStyles ) ) {
return;
}
const label =
registeredStyles.find( ( style ) => style.name === slug )
?.label || slug;
entries.push(
sprintf(
/* translators: %s: the name of a block style variation, e.g. "Plain". */
__( '%s variation' ),
label
)
);
}
);

if ( ! entries.length ) {
return '';
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No, not like this. Add random text here is not a good idea.


return sprintf(
/* translators: %s: a list of the styles a user changed, e.g. "Typography, Colors". */
__( 'Changed: %s' ),
entries.join(
/* translators: Used between list items, there is a space after the comma. */
__( ', ' ) // eslint-disable-line @wordpress/i18n-no-flanking-whitespace
)
);
}

type StyleFilter = 'all' | 'customized';

function useSortedBlockTypes() {
Expand Down Expand Up @@ -141,10 +216,10 @@ export function useBlockHasGlobalStyles( blockName: string ) {

interface BlockMenuItemProps {
block: any;
isCustomized: boolean;
summary: string;
}

function BlockMenuItem( { block, isCustomized }: BlockMenuItemProps ) {
function BlockMenuItem( { block, summary }: BlockMenuItemProps ) {
const hasBlockMenuItem = useBlockHasGlobalStyles( block.name );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not for now, but potentially a refactor idea: this currently places the check of whether or not to render within BlockMenuItem rather than further up the hierarchy where we show an empty state. So, it's theoretically possible (but unlikely) to have a situation where the list renders completely empty (because every item in the list returns false for this check) rather than show the empty state text.

So, at some point, we might want the check to occur at the same place where we do the filtering. Just a thought for later, nothing blocking for now!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point. Hoisting might change a few files, so I'll do it a follow up. 👍🏻

if ( ! hasBlockMenuItem ) {
return null;
Expand All @@ -154,20 +229,23 @@ function BlockMenuItem( { block, isCustomized }: BlockMenuItemProps ) {
<NavigationButtonAsItem
path={ '/blocks/' + encodeURIComponent( block.name ) }
>
<HStack justify="flex-start">
<BlockIcon icon={ block.icon } />
<FlexItem>{ block.title }</FlexItem>
{ isCustomized && (
<>
<VisuallyHidden>
{ __( 'Has custom styles' ) }
</VisuallyHidden>
<span
aria-hidden="true"
className="global-styles-ui-block-types-item__indicator"
/>
</>
) }
<HStack
justify="flex-start"
alignment={ !! summary ? 'flex-start' : 'center' }
spacing={ 2 }
>
<BlockIcon
className="global-styles-ui-block-types-item__icon"
icon={ block.icon }
/>
<FlexItem>
{ block.title }
{ !! summary && (
<span className="global-styles-ui-block-types-item__summary">
{ summary }
</span>
) }
</FlexItem>
</HStack>
</NavigationButtonAsItem>
);
Expand Down Expand Up @@ -197,24 +275,32 @@ interface BlockListProps {
function BlockList( { filterValue, styleFilter }: BlockListProps ) {
const sortedBlockTypes = useSortedBlockTypes();
const debouncedSpeak = useDebounce( speak, 500 );
const { isMatchingSearchTerm } = useSelect( blocksStore );
const { isMatchingSearchTerm, getBlockStyles } = useSelect( blocksStore );
const { user } = useContext( GlobalStylesContext );

// Computed once for the whole list rather than per row, so the list does
// not open a context subscription for every registered block.
const customizedBlockNames = useMemo( () => {
const names = new Set< string >();
const blockNames = [
// Computed once for the whole list rather than per row, so the list does not
// open a context subscription for every registered block. Maps a customized
// block's name to its summary, which can be an empty string.
const customizedBlocks = useMemo( () => {
const summaries = new Map< string, string >();
const blockNames = new Set( [
...Object.keys( user?.styles?.blocks ?? {} ),
...Object.keys( user?.settings?.blocks ?? {} ),
];
] );
blockNames.forEach( ( blockName ) => {
if ( hasUserStylesForBlock( user, blockName ) ) {
names.add( blockName );
summaries.set(
blockName,
getUserStylesSummary(
user,
blockName,
getBlockStyles( blockName )
)
);
}
} );
return names;
}, [ user ] );
return summaries;
}, [ user, getBlockStyles ] );

const searchedBlockTypes = ! filterValue
? sortedBlockTypes
Expand All @@ -225,7 +311,7 @@ function BlockList( { filterValue, styleFilter }: BlockListProps ) {
const filteredBlockTypes =
styleFilter === 'customized'
? searchedBlockTypes.filter( ( blockType ) =>
customizedBlockNames.has( blockType.name )
customizedBlocks.has( blockType.name )
)
: searchedBlockTypes;

Expand Down Expand Up @@ -266,7 +352,7 @@ function BlockList( { filterValue, styleFilter }: BlockListProps ) {
filteredBlockTypes.map( ( block ) => (
<BlockMenuItem
block={ block }
isCustomized={ customizedBlockNames.has( block.name ) }
summary={ customizedBlocks.get( block.name ) ?? '' }
key={ 'menu-itemblock-' + block.name }
/>
) )
Expand Down
27 changes: 18 additions & 9 deletions packages/global-styles-ui/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,26 @@
flex-grow: 1;
}

.global-styles-ui-block-types-item__indicator {
background: var(--wp-admin-theme-color);
border-radius: 50%;
flex: none;
height: 6px;
margin-left: auto;
width: 6px;
.global-styles-ui-block-types-item-list {
.global-styles-ui-block-types-item__icon {
min-width: variables.$grid-unit-30;
}
}

.global-styles-ui-block-types-item-list__no-results {
padding-top: variables.$grid-unit-20;
.global-styles-ui-block-types-item {
&__no-results {
padding-top: variables.$grid-unit-20;
}

&__summary {
color: colors.$gray-700;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
margin: variables.$grid-unit-05 0;
}
}

.global-styles-ui-screen-typography__font-variants-count {
Expand Down
Loading