Skip to content

Interactivity API: Preserve client-injected styles via data-wp-router-managed - #81452

Draft
DAreRodz wants to merge 2 commits into
trunkfrom
try/router-managed-attribute
Draft

Interactivity API: Preserve client-injected styles via data-wp-router-managed#81452
DAreRodz wants to merge 2 commits into
trunkfrom
try/router-managed-attribute

Conversation

@DAreRodz

Copy link
Copy Markdown
Contributor

What?

See #76031.

Make the Interactivity API router honor the data-wp-router-managed attribute during client-side navigation. Style assets that lack the attribute — on pages that otherwise use it — were injected by client scripts and are left untouched when navigating: never disabled, never re-enabled.

This is the client-side counterpart of the server experiment in WordPress/wordpress-develop#12963, which adds an empty data-wp-router-managed attribute to every <style> and <link rel="stylesheet"> element in the generated HTML when the page supports client-side navigation.

Why?

The router currently disables every style element not present in the incoming page, including elements it didn't render: styles injected at runtime by consent managers, theme/dark-mode switchers, a11y toolbars, or lazily loaded CSS disappear on the first client-side navigation.

The router can't infer ownership reliably on its own (see the discussion in #76031). With the server marking its own style assets, ownership becomes a synchronous per-element attribute check — no heuristics, no re-fetching, no races.

How?

All changes live in @wordpress/interactivity-router:

  • initRouterManagedMode() runs once at module evaluation: the router enters "managed mode" if the initial document contains at least one marked style asset, and eagerly claims all marked elements as server-managed. Eager claiming matters because the initial page is prepared only after every region has hydrated (initialVdomPromise), while a navigation only needs the region that triggers it: on a page with many islands, a click on an early-hydrated region with the target page already prefetched can render before the initial page preparation runs.
  • prepareStylePromise() classifies elements stickily: elements from fetched documents are server-managed by definition; elements already live and unmarked (in managed mode) are client-owned and never claimed.
  • applyStyles() in managed mode only disables server-managed elements, and only re-enables elements it disabled itself, so a stylesheet disabled by a client script keeps its state. Without markers, the previous behavior is unchanged — full backward compatibility with WordPress versions that don't emit the attribute.
  • normalizeMedia() ignores the attribute when comparing elements, so navigating between marked and unmarked pages reuses equivalent stylesheets instead of duplicating them.
  • The mode is fixed at initialization and never flips: marked → unmarked navigations keep preserving client-injected styles; an unmarked initial page keeps the previous behavior even when a fetched page is marked (the safe failure direction).

The WordPress version bundled with wp-env doesn't emit the attribute yet, so the new e2e test plugin (test/router-styles-managed) simulates it with an output buffer that marks all style assets via WP_HTML_Tag_Processor, keeping the tests self-contained.

Testing Instructions

  1. Run npm run test:unit -- packages/interactivity-router.
  2. Start the test environment (npm run wp-env-test start) and build
    (npm run build).
  3. Run npm run test:e2e -- test/e2e/specs/interactivity/router-styles-managed.spec.ts for the new behavior, and npm run test:e2e -- test/e2e/specs/interactivity/router-styles.spec.ts to confirm unmarked pages keep the current behavior.

To test manually:

  1. Create a post with the test/router-styles-managed block (linking to a second post, see the e2e spec setup).
  2. Open it on the front end.
  3. Inject a stylesheet from the console.
    document.head.appendChild(
      Object.assign(
        document.createElement( 'style' ),
        { textContent: 'body { outline: 2px solid red; }' }
      )
    )
    
  4. Navigate: the injected style persists. On trunk, it is disabled after the first navigation.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Fable 5, Claude Opus 5
Used for: Implementation, tests, adversarial review, and drafting this description.

@github-actions github-actions Bot added the [Package] Interactivity Router /packages/interactivity-router label Aug 11, 2026
@DAreRodz DAreRodz added [Type] Enhancement A suggestion for improvement. [Feature] Interactivity API API to add frontend interactivity to blocks. labels Aug 11, 2026
@github-actions

Copy link
Copy Markdown

Size Change: +128 B (0%)

Total Size: 7.71 MB

📦 View Changed
Filename Size Change
build/modules/interactivity-router/index.min.js 12.1 kB +128 B (+1.07%)

compressed-size-action

@github-actions

Copy link
Copy Markdown

Flaky tests detected in 28f82b5.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/31522779853
📝 Reported tests:

should render an autosave that is absent from the revisions collection in /test/e2e/specs/editor/various/revisions.spec.js, passed after 1 failed attempt.
Error: expect(locator).toBeVisible() failed

Locator: getByRole('button', { name: 'Restore' })
Expected: visible
Timeout: 5000ms
Error: element(s) not found

Call log:
  - Expect "toBeVisible" with timeout 5000ms
  - waiting for getByRole('button', { name: 'Restore' })

    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/revisions.spec.js:993:5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Interactivity API API to add frontend interactivity to blocks. [Package] Interactivity Router /packages/interactivity-router [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant