fix(modals): guard wire-elements/modal close path against activeComponent/components desync
Wrap window.LivewireUIModal at alpine:init to null-guard closingModal() the way the vendor already guards getActiveComponentModalAttribute, so the click-away path no longer throws "Cannot read properties of undefined (reading 'name')" when @persist widens the Alpine/Livewire desync window. In-sync close behavior (incl. a modal vetoing its own close) is unchanged. Verified: deterministic browser repro flips threw:true -> threw:false; the real closeModalOnClickAway() entry is clean under forced desync and the modal still closes; @persist first-open after navigate, spinner, and Escape/Abbrechen all remain clean (0 console errors). 182 tests pass; Vite build OK; Codex review approved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
e58a08b974
commit
62158eecdc
|
|
@ -77,6 +77,47 @@ document.addEventListener('livewire:init', () => {
|
|||
});
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
// ── wire-elements/modal close-path guard (vendor bug workaround) ─────
|
||||
// The package's modal.js closingModal() reads components[activeComponent].name
|
||||
// with NO null-guard — unlike its sibling getActiveComponentModalAttribute(),
|
||||
// which DOES guard the very same access (`components[activeComponent] !== undefined`).
|
||||
// When Alpine's `activeComponent` and the Livewire-side `components` map are
|
||||
// momentarily out of sync at close time — a window that @persist'ing the modal
|
||||
// manager across wire:navigate widens (the manager + its Alpine state survive a
|
||||
// navigation while Livewire re-hydrates `components`) — that read throws
|
||||
// TypeError: Cannot read properties of undefined (reading 'name')
|
||||
// on the click-away path (closeModalOnClickAway → closingModal). The modal still
|
||||
// closes, so it is console noise, but it violates R12 (zero console errors).
|
||||
//
|
||||
// We wrap the global factory to apply the SAME guard the vendor already uses
|
||||
// elsewhere: when there is no live component for the active id there is nothing to
|
||||
// notify and nothing can veto the close, so we report "closing allowed" (return
|
||||
// true) and let closeModal() proceed exactly as before. The in-sync path is
|
||||
// untouched (delegates to the original closingModal, preserving a modal's ability
|
||||
// to veto its own close). Runs at alpine:init — before Alpine evaluates the
|
||||
// persisted manager's x-data="LivewireUIModal()" — so every instance is guarded.
|
||||
// Keeps the @persist navigate fix and the <x-modal-trigger> spinner/error-toast UX
|
||||
// intact (this file is the only thing touched).
|
||||
const modalFactory = window.LivewireUIModal;
|
||||
if (typeof modalFactory === 'function' && !modalFactory.__clusevGuarded) {
|
||||
const guarded = function () {
|
||||
const data = modalFactory();
|
||||
const original = data.closingModal;
|
||||
if (typeof original === 'function') {
|
||||
data.closingModal = function (eventName) {
|
||||
const components = this.$wire?.get('components');
|
||||
if (!components || components[this.activeComponent] === undefined) {
|
||||
return true; // desync / already torn down: nothing to notify, allow close
|
||||
}
|
||||
return original.call(this, eventName);
|
||||
};
|
||||
}
|
||||
return data;
|
||||
};
|
||||
guarded.__clusevGuarded = true;
|
||||
window.LivewireUIModal = guarded;
|
||||
}
|
||||
|
||||
// ── Modal trigger ───────────────────────────────────────────────────
|
||||
// Reused by <x-modal-trigger>. Gives EVERY modal-opening control an immediate
|
||||
// spinner (pending flips synchronously on click — no round-trip needed) that clears
|
||||
|
|
|
|||
Loading…
Reference in New Issue