[6.x] Fix Utility pages not rendering - #19340
Open
brianjhanson wants to merge 5 commits into
Open
Conversation
Two independent defects left every Utility page blank. `slideout.ts` built its shared live region with `$(...)` at module scope. `cp.ts` imports the module on every Inertia CP page, so on any page where jQuery isn't already loaded the import throws, aborting the CP bootstrap before `window.Cp = Cp` runs — hence `window.Cp.config is not a function` and an Inertia app that never mounts. The region is now built on first use. `UtilitiesController::show()` called `headHtml()`/`bodyHtml()`, which clear the registry as they render. That drained not just the utility's own registrations but the global CP asset bundle (jQuery, Garnish, legacy cp.js, the `window.Craft` config) that `HandleInertiaRequests` registers for every CP request, into page props. `app.blade.php` was left with an empty `<head>`, so the legacy stack never loaded. It now uses `HtmlStack::capture()` so only the utility's own assets go to the props, where `AppLayout`'s `useAppendHtml()` already applies them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📚 Storybook previews@craftcms/ui — open Storybook No changed components detected in this Storybook. resources/js — open Storybook No changed components detected in this Storybook. |
Replaces the lazy `$('<span …>')` getter with a plain `document.createElement`
at module scope, matching how the modern Garnish `Modal` port builds its own
live region. This removes jQuery from module evaluation outright rather than
deferring the call, which is what actually kept `cp.ts` from booting on pages
without the legacy bundle.
`init` still wraps the shared node with `$()` before assigning `$liveRegion`:
`CP.js`'s announcer reads that property off the instance and calls `.empty()`,
`.text()` and `[0]` on it, so the exposed value has to stay a jQuery
collection. Wrapping the same node also preserves the moves-not-clones
semantic — `appendTo` still relocates the single shared element.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removes the last thing forcing `Slideout.$liveRegion` to be a jQuery collection, so it can be a plain element like the ported Garnish `Modal`'s. `announce()` and `handleLayerUpdates()` reached into `$activeLiveRegion` with `[0]`, `.empty()` and `.text()`, which meant every component feeding the announcer had to keep handing it jQuery. Both now resolve the reference through `getLiveRegionElement()` and set `textContent`, accepting either a jQuery collection (the jQuery-era modals and `$globalLiveRegion`) or a plain element. `textContent = message` replaces `.empty().text(message)` — it clears and sets in one step. `Slideout.$liveRegion` is now the shared element itself, appended with `appendChild`, which still moves rather than clones it. Verified in the browser against both shapes: the old announcer silently warns and announces nothing when given an element, while the new one announces through both it and a jQuery collection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brianjhanson
marked this pull request as ready for review
July 29, 2026 18:46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Utility pages rendered blank, with
$ is not defined,window.Cp.config is not a function, and a second$ is not definedfrom the page's ready-JS in the console. Two independent defects, plus a jQuery-removal cleanup they opened up.resources/js/modules/slideout/slideout.tsbuilt its shared live region with$('<span …>')at module scope.cp.tsimports the module on every Inertia CP page, so wherever jQuery isn't already on the page the import throws, aborting the CP bootstrap beforewindow.Cp = Cpruns — which is whywindow.Cp.configwas missing and the Inertia app never mounted. The region is now built withdocument.createElement, the same way the ported GarnishModalbuilds its own.UtilitiesController::show()calledheadHtml()/bodyHtml(), which clear the registry as they render. That drained not just the utility's own registrations but the global CP asset bundle — jQuery, Garnish, legacycp.js, thewindow.Craftconfig — thatHandleInertiaRequestsregisters for every CP request, into page props thatutilities/Show.vuedoesn't read.app.blade.phpwas left with an effectively empty<head>(571 bytes on/utilities/*vs ~22KB on/dashboard), so the legacy stack never loaded and the ready-JS wrapper hit an undefined$. It now usesHtmlStack::capture()so only the utility's own assets reach the props, whereAppLayout's existinguseAppendHtml()applies them — on the initial render and on subsequent Inertia visits, which never re-render the root template.Craft.cp.announce()reached into$activeLiveRegionwith[0],.empty()and.text(), which forced every component feeding the announcer to keep handing it jQuery —Slideout.$liveRegionincluded. It now resolves the reference through agetLiveRegionElement()helper and assignstextContent, accepting either a jQuery collection (the jQuery-era modals and$globalLiveRegion) or a plain element. That letsSlideout.$liveRegionbecome the shared element itself, appended withappendChild, which still moves rather than clones it.Verification
Reverted both fixes and reproduced all three console errors verbatim against a local build, then confirmed they're gone and the page renders fully. All 9 utility pages carry the complete asset stack.
For the announcer, exercised both live-region shapes against both implementations:
announce()announce()A real
Craft.Slideoutnow exposes$liveRegionas anHTMLElement, still renders as<span class="visually-hidden" role="status"></span>, still moves as a single shared node between containers, and announces correctly.tests/Feature/Integration/UtilityPageAssetsTest.phppins the controller regression — it fails 3/4 without that change. Garnish (351) and root (41) Vitest suites pass;vue-tscis clean.Note: the legacy CP bundle could not be rebuilt cleanly in my environment —
npm run build:bundlesfails loadinginputmask/webpack.config.js(pkgDir.sync is not a function), and building thecpbundle alone fails on./js/FieldToggle.jsimportingtailwindcss/src/value-parser. Both are pre-existing and unrelated to this diff, but they mean theCP.jschange was verified by running its exact logic in the browser rather than through a fully-built bundle.🤖 Generated with Claude Code