From f31591e23ef656634cc4d43406480b8cb37e55a8 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 15 Jun 2026 18:08:32 +0200 Subject: [PATCH 1/7] ThemeProvider: apply scoped tokens via inline style Replaces the per-instance ` - ) : null } -
- - { children } - -
- +
+ + { children } + +
); }; From fa2f309cf36e08155408a18ae167a0fad1ec70f2 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 15 Jun 2026 18:14:43 +0200 Subject: [PATCH 2/7] ThemeProvider: keep data-wpds-root-provider marker for CSS forwarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores `data-wpds-root-provider="true"` on the wrapper when `isRoot`, so prebuilt CSS can forward preset-based settings (e.g. `cornerRadius` in #79153) to `:root` via `:root:has([data-wpds-root-provider="true"]…)`. Dynamic color/cursor values keep using the JS sync in `useLayoutEffect` (they can't be expressed in a static stylesheet). Comment updated to explain the deliberate split. --- packages/theme/src/theme-provider.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/theme/src/theme-provider.tsx b/packages/theme/src/theme-provider.tsx index 8f512b8b00807f..9510f11a4eaa80 100644 --- a/packages/theme/src/theme-provider.tsx +++ b/packages/theme/src/theme-provider.tsx @@ -31,11 +31,16 @@ export const ThemeProvider = ( { [ resolvedSettings ] ); - // Mirror the wrapper's custom properties onto `document.documentElement` - // so they reach portals and anything else rendered outside the wrapper - // (e.g. the `html`/`body` background). Unlike the wrapper, `html` is a - // shared element, so we set/remove individual properties (preserving any - // prior value) instead of declaratively assigning a full style object. + // Mirror the wrapper's dynamic custom properties (color/cursor) onto + // `document.documentElement` so they reach portals and anything else + // rendered outside the wrapper (e.g. the `html`/`body` background, or + // PHP-rendered admin UI alongside the React app). Preset-based settings + // (e.g. `cornerRadius`) are forwarded declaratively by the prebuilt CSS + // via `:root:has([data-wpds-root-provider="true"]…)`; only the per-seed + // values that can't be expressed in a static stylesheet are synced here. + // Unlike the wrapper, `html` is a shared element, so we set/remove + // individual properties (preserving any prior value) instead of + // declaratively assigning a full style object. useLayoutEffect( () => { if ( ! isRoot || typeof document === 'undefined' ) { return; @@ -73,6 +78,7 @@ export const ThemeProvider = ( { return (
Date: Wed, 17 Jun 2026 00:07:07 +0200 Subject: [PATCH 3/7] ThemeProvider: test document-root cleanup on root provider unmount Adds a direct assertion that unmounting an `isRoot` provider removes the forwarded custom properties from `document.documentElement`, exercising the `useLayoutEffect` cleanup path explicitly instead of relying on cross-test RTL auto-cleanup coupling. --- .../theme/src/test/theme-provider.test.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/theme/src/test/theme-provider.test.tsx b/packages/theme/src/test/theme-provider.test.tsx index da00b376647b9f..6b50248ed18907 100644 --- a/packages/theme/src/test/theme-provider.test.tsx +++ b/packages/theme/src/test/theme-provider.test.tsx @@ -131,6 +131,24 @@ describe( 'ThemeProvider', () => { expect( readProp( document.documentElement, BRAND_BG ) ).toBe( '' ); } ); + it( 'removes the forwarded properties from the document root on unmount', () => { + const { unmount } = render( + +
x
+
+ ); + + expect( readProp( document.documentElement, BRAND_BG ) ).toBe( + PRIMARY + ); + + unmount(); + + // The effect cleanup restores the previous value — here none, so + // the property is removed rather than left stuck on the seed. + expect( readProp( document.documentElement, BRAND_BG ) ).toBe( '' ); + } ); + // Unlike color/cursor (applied as inline custom properties on the // provider's wrapper), the `cornerRadius` preset resolves to // `--wpds-border-radius-*` values through the prebuilt design-token CSS From d4c61b0292d1120bf2fffcd7f7e6803f639a0b65 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Wed, 17 Jun 2026 00:17:55 +0200 Subject: [PATCH 4/7] ThemeProvider: shorten code comments --- .../theme/src/test/theme-provider.test.tsx | 21 +++++++------------ packages/theme/src/theme-provider.tsx | 15 +++++-------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/theme/src/test/theme-provider.test.tsx b/packages/theme/src/test/theme-provider.test.tsx index 6b50248ed18907..63592db1f49709 100644 --- a/packages/theme/src/test/theme-provider.test.tsx +++ b/packages/theme/src/test/theme-provider.test.tsx @@ -8,9 +8,8 @@ import { join } from 'path'; import { render, screen } from '@testing-library/react'; import { ThemeProvider } from '../theme-provider'; -// Mock the CSS module so the provider's wrapper has a stable class. The tests -// use this class as the scoping selector (in place of any instance-specific -// attribute) and to read computed custom properties from the wrapper itself. +// Give the wrapper a stable class so tests can locate it and read its +// computed custom properties. jest.mock( '../style.module.css', () => ( { root: 'theme-provider-root', } ) ); @@ -144,20 +143,14 @@ describe( 'ThemeProvider', () => { unmount(); - // The effect cleanup restores the previous value — here none, so - // the property is removed rather than left stuck on the seed. + // No prior value, so cleanup removes the property entirely. expect( readProp( document.documentElement, BRAND_BG ) ).toBe( '' ); } ); - // Unlike color/cursor (applied as inline custom properties on the - // provider's wrapper), the `cornerRadius` preset resolves to - // `--wpds-border-radius-*` values through the prebuilt design-token CSS - // (the modes authored in `terrazzo.config.ts`). A root provider relies - // on that stylesheet's `:root:has( [data-wpds-root-provider='true']… )` - // rule to forward the preset to the document element, so these tests - // load the prebuilt CSS to exercise it. It is injected only for this - // block (it also defines base tokens on `:root`) to avoid interfering - // with the color assertions above. + // `cornerRadius` forwards to `:root` through the prebuilt CSS's + // `:root:has( [data-wpds-root-provider='true']… )` rule (not the JS + // mirror used for color/cursor), so load that stylesheet to exercise + // it. Scoped to this block since it also defines base `:root` tokens. describe( 'cornerRadius forwarding', () => { let prebuiltStyle: HTMLStyleElement; diff --git a/packages/theme/src/theme-provider.tsx b/packages/theme/src/theme-provider.tsx index 9510f11a4eaa80..37bf3ad4378573 100644 --- a/packages/theme/src/theme-provider.tsx +++ b/packages/theme/src/theme-provider.tsx @@ -31,16 +31,11 @@ export const ThemeProvider = ( { [ resolvedSettings ] ); - // Mirror the wrapper's dynamic custom properties (color/cursor) onto - // `document.documentElement` so they reach portals and anything else - // rendered outside the wrapper (e.g. the `html`/`body` background, or - // PHP-rendered admin UI alongside the React app). Preset-based settings - // (e.g. `cornerRadius`) are forwarded declaratively by the prebuilt CSS - // via `:root:has([data-wpds-root-provider="true"]…)`; only the per-seed - // values that can't be expressed in a static stylesheet are synced here. - // Unlike the wrapper, `html` is a shared element, so we set/remove - // individual properties (preserving any prior value) instead of - // declaratively assigning a full style object. + // For root providers, mirror the wrapper's custom properties onto `html` + // so they reach portals and content outside the React app. `html` is + // shared, so set/remove individual properties (restoring any prior value) + // rather than assigning a whole style object. (Preset settings like + // `cornerRadius` are forwarded by the prebuilt CSS instead.) useLayoutEffect( () => { if ( ! isRoot || typeof document === 'undefined' ) { return; From aaa746268e4d2bfecbbdd32a68ad61c11c2e3d98 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 22 Jun 2026 14:42:54 +0200 Subject: [PATCH 5/7] ThemeProvider: Scope root forwarding to wrapper's own document; warn on multiple roots For root providers, resolve the target document element from the wrapper's ownerDocument so forwarding works correctly when the provider is rendered inside an iframe (instead of always writing to the top-level document). Add a dev-only warning when more than one root provider is mounted on the same document, since their forwarded document-level styles would conflict. Also move the inline-style CHANGELOG entry under Code Quality, alongside the related data-wpds-root-provider serialization change. --- packages/theme/CHANGELOG.md | 2 +- .../theme/src/test/theme-provider.test.tsx | 60 +++++++++++++++++++ packages/theme/src/theme-provider.tsx | 52 +++++++++++++--- 3 files changed, 104 insertions(+), 10 deletions(-) diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index f239c4913fa5eb..22bbee41b80631 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -38,7 +38,6 @@ - Add unit tests for `ThemeProvider` and `useThemeProviderStyles` ([#79126](https://github.com/WordPress/gutenberg/pull/79126)). - Run the stylelint plugin tests through the stylelint Node API instead of spawning the CLI via `child_process` ([#79199](https://github.com/WordPress/gutenberg/pull/79199)). -- `ThemeProvider`: Apply scoped custom properties via inline `style` (and `document.documentElement.style` when `isRoot`) instead of a per-instance `