From 00abf1180fa7197dff11bbcd765ed11babba8343 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 21 Jul 2026 12:46:34 +1000 Subject: [PATCH 1/4] Global styles panels: commit the inherited slug when accepting an inherited color Clicking the preselected swatch to accept an inherited color committed only the decoded value. Re-encoding by hex picks the first palette entry with that value, so with two presets sharing a hex (e.g. dark-background and dark-text both #000000) the accepted dark-text was saved as dark-background. Pass the inherited slug alongside the value so the encoder short-circuits to the reference the at-rest UI was showing. --- .../color-gradient-dropdown-item.js | 2 +- .../global-styles/test/typography-panel.js | 44 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js b/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js index d87c10d0201429..b4a1cfff22b60a 100644 --- a/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js +++ b/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js @@ -151,7 +151,7 @@ function ColorGradientTab( { // set, the same click correctly clears local back to at rest. const onChange = ( newValue, newSlug ) => { if ( isPlaceholder && newValue === undefined ) { - setValue( inheritedValue ); + setValue( inheritedValue, inheritedSlug ); return; } setValue( newValue, newSlug ); diff --git a/packages/block-editor/src/components/global-styles/test/typography-panel.js b/packages/block-editor/src/components/global-styles/test/typography-panel.js index f813997e13b487..18d18dfbac03ca 100644 --- a/packages/block-editor/src/components/global-styles/test/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/test/typography-panel.js @@ -739,16 +739,44 @@ const DUPLICATE_PALETTE_SETTINGS = { }, }; -describe( 'TypographyPanel — setTextColor link sync', () => { - // Helper: open the text Color dropdown and return the rendered swatches. - async function openTextColorDropdown() { - await click( - screen.getByRole( 'button', { name: /Color/, expanded: false } ) +// Helper: open the text Color dropdown and return the rendered swatches. +async function openTextColorDropdown() { + await click( + screen.getByRole( 'button', { name: /Color/, expanded: false } ) + ); + // `findAllByRole` waits for the Popover/portal content to appear. + return screen.findAllByRole( 'option' ); +} + +describe( 'TypographyPanel — duplicate-hex preset slug identity', () => { + it( 'commits the inherited preset slug when accepting the preselected inherited color', async () => { + const onChange = jest.fn(); + + await renderAriakit( + ); - // `findAllByRole` waits for the Popover/portal content to appear. - return screen.findAllByRole( 'option' ); - } + const swatches = await openTextColorDropdown(); + // swatch[1] ('Dark Text') is the preselected inherited option; + // activating it is the "accept inherited value" gesture. The commit + // must carry the inherited slug, not re-encode the shared #000 hex + // to whichever duplicate appears first in the palette. + await click( swatches[ 1 ] ); + + const result = onChange.mock.calls[ 0 ][ 0 ]; + expect( result?.color?.text ).toBe( 'var:preset|color|dark-text' ); + } ); +} ); + +describe( 'TypographyPanel — setTextColor link sync', () => { it( 'syncs the link color when text and link share the same raw preset reference', async () => { const onChange = jest.fn(); const sharedRef = 'var:preset|color|dark-background'; From 316594b1147b26d7264178b70d377720abc557d5 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 21 Jul 2026 12:49:56 +1000 Subject: [PATCH 2/4] Global styles panels: match picker selection by slug for local preset values Once a local value was set the picker dropped its slug and fell back to hex matching, so two presets sharing a hex both rendered as selected and the first one appeared to be the stored value. Pass the local value's slug through each color tab and select by it, mirroring the existing inherited-slug selection at rest. A slug-less custom value keeps the hex fallback. --- .../global-styles/background-panel.js | 29 ++++++------ .../color-gradient-dropdown-item.js | 12 ++--- .../components/global-styles/color-panel.js | 16 +++++++ .../global-styles/test/color-panel.js | 45 +++++++++++++++++++ .../global-styles/test/typography-panel.js | 17 +++++++ .../global-styles/typography-panel.js | 17 ++++--- 6 files changed, 106 insertions(+), 30 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js index 2ea84afa9309e0..8b80dfde39e1bf 100644 --- a/packages/block-editor/src/components/global-styles/background-panel.js +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -400,22 +400,19 @@ export default function BackgroundImagePanel( { key: 'background', label: __( 'Color' ), inheritedValue: backgroundColor, - // Resolve the slug from the same source as the - // displayed value (user value first, then the - // inherited fallback). For a block instance the - // selection lives in `value` while `inheritedValue` - // only holds the global styles fallback, so reading - // the slug from `inheritedValue` alone would miss it - // and two same-hex presets would both appear selected. - inheritedSlug: - extractPresetSlug( - value?.color?.background, - 'color' - ) ?? - extractPresetSlug( - inheritedValue?.color?.background, - 'color' - ), + // The picker matches selection by slug, following + // whichever value is displayed: `localSlug` once a + // value is set, `inheritedSlug` at rest. Without + // the slugs it falls back to hex matching, which + // marks two same-hex presets as both selected. + inheritedSlug: extractPresetSlug( + inheritedValue?.color?.background, + 'color' + ), + localSlug: extractPresetSlug( + value?.color?.background, + 'color' + ), setValue: setBackgroundColor, userValue: userBackgroundColor, isPlaceholder: diff --git a/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js b/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js index b4a1cfff22b60a..60151f99e9ead9 100644 --- a/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js +++ b/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js @@ -132,6 +132,7 @@ function ColorGradientTab( { isGradient, inheritedValue, inheritedSlug, + localSlug, userValue, setValue, isPlaceholder, @@ -142,6 +143,11 @@ function ColorGradientTab( { // back to the inherited value so the at-rest preselection is visible // inside the picker. const displayed = userValue ?? inheritedValue; + // Selection inside the picker follows the slug of whichever value is + // displayed: the local slug once a value is set, the inherited slug at + // rest. Matching by slug keeps two same-hex presets apart; a slug-less + // (custom) value falls back to the picker's own hex matching. + const displayedSlug = userValue !== undefined ? localSlug : inheritedSlug; // Display-without-commit interceptor. `ColorPalette` and `GradientPicker` // fire `onChange( undefined )` when the user clicks the currently-selected // option. At rest (placeholder mode), that click is the user's "accept @@ -163,11 +169,7 @@ function ColorGradientTab( { enableAlpha __experimentalIsRenderedInSidebar colorValue={ isGradient ? undefined : displayed } - colorSlug={ - isGradient || userValue !== undefined - ? undefined - : inheritedSlug - } + colorSlug={ isGradient ? undefined : displayedSlug } gradientValue={ isGradient ? displayed : undefined } onColorChange={ isGradient ? undefined : onChange } onGradientChange={ isGradient ? onChange : undefined } diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index 52aa87e2058174..cc964eb3f6b35e 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -324,6 +324,10 @@ export default function ColorPanel( { inheritedValue?.elements?.link?.color?.text, 'color' ), + localSlug: extractPresetSlug( + value?.elements?.link?.color?.text, + 'color' + ), setValue: setLinkColor, userValue: userLinkColor, isPlaceholder: @@ -338,6 +342,10 @@ export default function ColorPanel( { ?.text, 'color' ), + localSlug: extractPresetSlug( + value?.elements?.link?.[ ':hover' ]?.color?.text, + 'color' + ), setValue: setHoverLinkColor, userValue: userHoverLinkColor, isPlaceholder: @@ -476,6 +484,10 @@ export default function ColorPanel( { inheritedValue?.elements?.[ name ]?.color?.text, 'color' ), + localSlug: extractPresetSlug( + value?.elements?.[ name ]?.color?.text, + 'color' + ), setValue: setElementTextColor, userValue: elementTextUserColor, isPlaceholder: isElementTextPlaceholder, @@ -490,6 +502,10 @@ export default function ColorPanel( { ?.background, 'color' ), + localSlug: extractPresetSlug( + value?.elements?.[ name ]?.color?.background, + 'color' + ), setValue: setElementBackgroundColor, userValue: elementBackgroundUserColor, isPlaceholder: isElementBackgroundPlaceholder, diff --git a/packages/block-editor/src/components/global-styles/test/color-panel.js b/packages/block-editor/src/components/global-styles/test/color-panel.js index 3f557998e8d287..063e60b20e47f5 100644 --- a/packages/block-editor/src/components/global-styles/test/color-panel.js +++ b/packages/block-editor/src/components/global-styles/test/color-panel.js @@ -2,6 +2,7 @@ * External dependencies */ import { render, renderHook, screen } from '@testing-library/react'; +import { click, render as renderAriakit } from '@ariakit/test/react'; /** * Internal dependencies @@ -209,6 +210,50 @@ const baseSettings = { }, }; +describe( 'ColorPanel — duplicate-hex preset slug identity', () => { + const duplicateHexSettings = { + color: { + link: true, + defaultPalette: false, + palette: { + theme: [ + { + slug: 'dark-background', + color: '#000000', + name: 'Dark background', + }, + { slug: 'dark-text', color: '#000000', name: 'Dark text' }, + ], + }, + }, + }; + + it( 'marks only the local link preset as selected when another preset shares its hex', async () => { + await renderAriakit( + {} } + panelId="test-panel" + /> + ); + + await click( screen.getByRole( 'button', { name: /^Link/ } ) ); + const swatches = await screen.findAllByRole( 'option' ); + + // swatch[0] = 'Dark background', swatch[1] = 'Dark text'. Selection + // must follow the stored slug; matching by hex would mark both. + expect( swatches[ 1 ] ).toHaveAttribute( 'aria-selected', 'true' ); + expect( swatches[ 0 ] ).toHaveAttribute( 'aria-selected', 'false' ); + } ); +} ); + describe( 'ColorPanel — inherited Global Styles label treatment', () => { describe( 'Link color', () => { it( 'exposes an accessible reset when local link.text overrides the inherited value', () => { diff --git a/packages/block-editor/src/components/global-styles/test/typography-panel.js b/packages/block-editor/src/components/global-styles/test/typography-panel.js index 18d18dfbac03ca..87b13e5cc2361d 100644 --- a/packages/block-editor/src/components/global-styles/test/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/test/typography-panel.js @@ -774,6 +774,23 @@ describe( 'TypographyPanel — duplicate-hex preset slug identity', () => { const result = onChange.mock.calls[ 0 ][ 0 ]; expect( result?.color?.text ).toBe( 'var:preset|color|dark-text' ); } ); + + it( 'marks only the local preset as selected when another preset shares its hex', async () => { + await renderAriakit( + + ); + + // swatch[0] = 'Dark Background', swatch[1] = 'Dark Text'. Selection + // must follow the stored slug; matching by hex would mark both. + const swatches = await openTextColorDropdown(); + expect( swatches[ 1 ] ).toHaveAttribute( 'aria-selected', 'true' ); + expect( swatches[ 0 ] ).toHaveAttribute( 'aria-selected', 'false' ); + } ); } ); describe( 'TypographyPanel — setTextColor link sync', () => { diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index 45dad0d1cfa4c8..a21f424ab86db1 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -822,15 +822,14 @@ export default function TypographyPanel( { key: 'text', label: __( 'Color' ), inheritedValue: textColor, - inheritedSlug: - extractPresetSlug( - value?.color?.text, - 'color' - ) ?? - extractPresetSlug( - inheritedValue?.color?.text, - 'color' - ), + inheritedSlug: extractPresetSlug( + inheritedValue?.color?.text, + 'color' + ), + localSlug: extractPresetSlug( + value?.color?.text, + 'color' + ), setValue: setTextColor, userValue: userTextColor, isPlaceholder: From 8e092f361d801a2ae51222926aa85b31bb20044d Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 21 Jul 2026 17:21:41 +1000 Subject: [PATCH 3/4] Global styles panels: rename localSlug to userSlug Matches the existing userValue/inheritedValue naming pair. --- .../src/components/global-styles/background-panel.js | 4 ++-- .../global-styles/color-gradient-dropdown-item.js | 6 +++--- .../src/components/global-styles/color-panel.js | 8 ++++---- .../src/components/global-styles/typography-panel.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js index 8b80dfde39e1bf..99f29587fee59e 100644 --- a/packages/block-editor/src/components/global-styles/background-panel.js +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -401,7 +401,7 @@ export default function BackgroundImagePanel( { label: __( 'Color' ), inheritedValue: backgroundColor, // The picker matches selection by slug, following - // whichever value is displayed: `localSlug` once a + // whichever value is displayed: `userSlug` once a // value is set, `inheritedSlug` at rest. Without // the slugs it falls back to hex matching, which // marks two same-hex presets as both selected. @@ -409,7 +409,7 @@ export default function BackgroundImagePanel( { inheritedValue?.color?.background, 'color' ), - localSlug: extractPresetSlug( + userSlug: extractPresetSlug( value?.color?.background, 'color' ), diff --git a/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js b/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js index 60151f99e9ead9..177171129582f2 100644 --- a/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js +++ b/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js @@ -132,7 +132,7 @@ function ColorGradientTab( { isGradient, inheritedValue, inheritedSlug, - localSlug, + userSlug, userValue, setValue, isPlaceholder, @@ -144,10 +144,10 @@ function ColorGradientTab( { // inside the picker. const displayed = userValue ?? inheritedValue; // Selection inside the picker follows the slug of whichever value is - // displayed: the local slug once a value is set, the inherited slug at + // displayed: the user slug once a value is set, the inherited slug at // rest. Matching by slug keeps two same-hex presets apart; a slug-less // (custom) value falls back to the picker's own hex matching. - const displayedSlug = userValue !== undefined ? localSlug : inheritedSlug; + const displayedSlug = userValue !== undefined ? userSlug : inheritedSlug; // Display-without-commit interceptor. `ColorPalette` and `GradientPicker` // fire `onChange( undefined )` when the user clicks the currently-selected // option. At rest (placeholder mode), that click is the user's "accept diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index cc964eb3f6b35e..9cec445dd6ce57 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -324,7 +324,7 @@ export default function ColorPanel( { inheritedValue?.elements?.link?.color?.text, 'color' ), - localSlug: extractPresetSlug( + userSlug: extractPresetSlug( value?.elements?.link?.color?.text, 'color' ), @@ -342,7 +342,7 @@ export default function ColorPanel( { ?.text, 'color' ), - localSlug: extractPresetSlug( + userSlug: extractPresetSlug( value?.elements?.link?.[ ':hover' ]?.color?.text, 'color' ), @@ -484,7 +484,7 @@ export default function ColorPanel( { inheritedValue?.elements?.[ name ]?.color?.text, 'color' ), - localSlug: extractPresetSlug( + userSlug: extractPresetSlug( value?.elements?.[ name ]?.color?.text, 'color' ), @@ -502,7 +502,7 @@ export default function ColorPanel( { ?.background, 'color' ), - localSlug: extractPresetSlug( + userSlug: extractPresetSlug( value?.elements?.[ name ]?.color?.background, 'color' ), diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index a21f424ab86db1..53e06d9c9fd9c3 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -826,7 +826,7 @@ export default function TypographyPanel( { inheritedValue?.color?.text, 'color' ), - localSlug: extractPresetSlug( + userSlug: extractPresetSlug( value?.color?.text, 'color' ), From 14f28cde0a882b67ae3f1a1765446163dbebba72 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 22 Jul 2026 10:59:13 +1000 Subject: [PATCH 4/4] Global styles panels: state the concrete condition in slug selection comments --- .../src/components/global-styles/background-panel.js | 9 ++++----- .../global-styles/color-gradient-dropdown-item.js | 7 +++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js index 99f29587fee59e..b5e08a89cab499 100644 --- a/packages/block-editor/src/components/global-styles/background-panel.js +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -400,11 +400,10 @@ export default function BackgroundImagePanel( { key: 'background', label: __( 'Color' ), inheritedValue: backgroundColor, - // The picker matches selection by slug, following - // whichever value is displayed: `userSlug` once a - // value is set, `inheritedSlug` at rest. Without - // the slugs it falls back to hex matching, which - // marks two same-hex presets as both selected. + // The picker selects by slug: `userSlug` when the + // block has its own value, otherwise + // `inheritedSlug`. Hex matching would mark two + // same-hex presets as both selected. inheritedSlug: extractPresetSlug( inheritedValue?.color?.background, 'color' diff --git a/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js b/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js index 177171129582f2..701e9e62951f3e 100644 --- a/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js +++ b/packages/block-editor/src/components/global-styles/color-gradient-dropdown-item.js @@ -143,10 +143,9 @@ function ColorGradientTab( { // back to the inherited value so the at-rest preselection is visible // inside the picker. const displayed = userValue ?? inheritedValue; - // Selection inside the picker follows the slug of whichever value is - // displayed: the user slug once a value is set, the inherited slug at - // rest. Matching by slug keeps two same-hex presets apart; a slug-less - // (custom) value falls back to the picker's own hex matching. + // Slug of the displayed value: the block's own when it has one, otherwise + // the inherited one. Slug matching keeps two same-hex presets apart; a + // slug-less (custom) value falls back to hex matching. const displayedSlug = userValue !== undefined ? userSlug : inheritedSlug; // Display-without-commit interceptor. `ColorPalette` and `GradientPicker` // fire `onChange( undefined )` when the user clicks the currently-selected