Skip to content

Prevent Unsaved Changes Data Loss on Desktop-Mode Iframe Window Close - #315

Merged
AllTerrainDeveloper merged 5 commits into
WordPress:trunkfrom
KarunyaChavan:fix/iframe-unsaved-changes-warning
Jul 8, 2026
Merged

Prevent Unsaved Changes Data Loss on Desktop-Mode Iframe Window Close#315
AllTerrainDeveloper merged 5 commits into
WordPress:trunkfrom
KarunyaChavan:fix/iframe-unsaved-changes-warning

Conversation

@KarunyaChavan

@KarunyaChavan KarunyaChavan commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

This PR prevents data loss when users close desktop-mode iframe windows (such as the Post Editor or Block Editor) that contain unsaved changes.
It introduces a cross-frame beforeunload negotiation protocol between the desktop shell and embedded iframe applications, ensuring editors can participate in the same unsaved-changes workflow used during normal WordPress navigation. The implementation also addresses several race conditions and failure scenarios to avoid orphaned or undestroyable windows.

Closes #314

Why

WordPress editors commonly register beforeunload handlers to warn users before navigating away with unsaved changes.

In desktop mode, however, iframe-based editor windows bypassed this mechanism. Clicking the native window close button (X) immediately destroyed the iframe, preventing the embedded application from executing its beforeunload logic and creating a data-loss path that did not exist in standard WordPress behavior.

During implementation, several correctness issues were also identified:

  • Re-entrant close() calls could trigger duplicate beforeunload negotiations and orphan safety timers.
  • Dynamic import failures could leave windows suspended in an undestroyable state.
  • postMessage communication used an unnecessarily permissive target origin ('*').
  • Late responses could arrive after a forced close and attempt to operate on already-destroyed windows.

Changes

Shell-side close negotiation

File: src/window/index.ts

  • Added a desktop-mode-bridge-beforeunload-query message sent to iframe windows before destruction.
  • Window teardown is suspended until the iframe responds.
  • Added _closePending to prevent duplicate negotiations from re-entrant close() calls.
  • Added _closePending: boolean state tracking.
  • Updated the safety timeout to verify _isDestroyed before proceeding.
  • Restricted postMessage target origin from '*' to location.origin.

Response handling

File: src/window/iframe-bridge.ts

  • Added handling for desktop-mode-bridge-beforeunload-response.

  • When prevent === true:

    • Dynamically imports and displays <wpd-confirm-dialog>.
    • User confirms → destroy().
    • User cancels → close operation is aborted.
  • When prevent === false:

    • Window is destroyed immediately.
  • Added _isDestroyed guards to ignore stale responses.

  • Added .catch() handling around dynamic imports.

  • Resets _closePending when negotiation completes.

Iframe bridge support

Files:

  • includes/render/chromeless-bridge.php
  • src/iframe-bridge-standalone.ts

Added support for handling desktop-mode-bridge-beforeunload-query by:

  1. Executing any registered window.onbeforeunload handler.
  2. Dispatching a synthetic DOM beforeunload event.
  3. Determining whether navigation should be prevented.
  4. Returning the result via desktop-mode-bridge-beforeunload-response.

TypeScript and lint fixes

File: src/iframe-bridge-standalone.ts

  • Renamed eve to satisfy no-shadow.
  • Fixed brace-style issues in getters/setters.
  • Added the required cast for Event.returnValue to satisfy TypeScript compilation.

How It Works

When a user clicks the close button on an iframe window:

  1. The shell sends a desktop-mode-bridge-beforeunload-query message to the iframe.

  2. The iframe synchronously executes its beforeunload logic.

  3. The iframe responds indicating whether closing should be prevented.

  4. If prevention is requested:

    • A confirmation dialog is displayed.
    • The user chooses whether to proceed.
  5. If prevention is not requested:

    • The window closes immediately.

This mirrors the protection users already receive during standard WordPress navigation.

Reliability Improvements

Data-loss prevention

Editors embedded inside desktop-mode iframe windows can now participate in the standard WordPress unsaved-changes workflow, preventing accidental loss of content.

Race-condition protection

  • Re-entrant close() calls are ignored while a negotiation is already in progress.
  • Safety timers cannot trigger duplicate destruction.
  • Responses arriving after a forced close are safely ignored.

Zombie-state prevention

  • Failed dynamic imports no longer leave windows permanently suspended.
  • If the iframe never responds, a 500ms safety timeout forces closure.
  • Import failures fall back to destroy() to ensure cleanup always completes.

Result

Desktop-mode iframe windows now provide the same unsaved-changes protection as standard WordPress navigation while remaining resilient to race conditions, import failures, stale responses, and non-responsive iframes.

Open WordPress Playground Preview

…t data loss.

- When closing an iframe-based window (Post Editor, Block Editor, etc.), the
shell now sends desktop-mode-bridge-beforeunload-query via postMessage and
suspends close pending a response (with 500ms safety timeout).
- Both the PHP chromeless-bridge and the standalone TS bridge handle the query
by synthesizing a beforeunload event on window.onbeforeunload + dispatching
a DOM beforeunload event.
- If unsaved changes are detected, a wpd-confirm-dialog
prompts the user before calling destroy().
…se flow.

- Add _closePending flag to prevent duplicate beforeunload queries and orphaned safety timers when close() is called re-entrantly.
- Add .catch() on dynamic wpdConfirm import so a module-load failure falls through to destroy() instead of leaving the window in a zombie state.
- Guard response handler and safety timeout against _isDestroyed to prevent double-teardown races.
- Use location.origin instead of '*' as postMessage targetOrigin.
- Fix iframe-bridge-standalone.ts no-shadow lint, brace-style, and Event.returnValue type cast.
@KarunyaChavan
KarunyaChavan marked this pull request as ready for review June 24, 2026 17:16
@AllTerrainDeveloper

Copy link
Copy Markdown
Collaborator

This is an amazing work, we'll test it and release it!

…es query, add test coverage + docs

- close() now no-ops on a second call while a beforeunload query is
  already in flight, instead of falling through to an immediate
  destroy (a double-click on the close button could previously skip
  the confirm dialog and lose unsaved changes silently).
- Add tests for the pre-close query flow (bridge-ready gating, safety
  timeout, native windows unaffected, the re-entrancy fix above) and
  for the beforeunload-response handler (confirm/cancel/fallback
  title/import-failure/already-destroyed/timeout cleanup).
- Document the new desktop-mode-bridge-beforeunload-* message pair in
  bridge-protocol.md and clarify in javascript-reference.md that
  iframe windows have their own separate pre-close guard from the
  native-only before-close filter.
@AllTerrainDeveloper

AllTerrainDeveloper commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this fix, really nice catch on the data-loss scenario! I went through it and added a bit on top:

  • Test coverage for the pre-close query flow and the beforeunload-response handling (confirm/cancel, the default-title fallback, the safety timeout, native windows being unaffected).
  • While writing those, found that calling close() a second time while a query is still in flight (e.g. a double-click on the close button) fell through to an immediate destroy, skipping the unsaved-changes check entirely — added a small fix + a regression test for that.
  • Documented the new desktop-mode-bridge-beforeunload-* messages in bridge-protocol.md, and added a note in javascript-reference.md clarifying that iframe windows now have their own pre-close guard separate from the native-only before-close filter.

@AllTerrainDeveloper
AllTerrainDeveloper merged commit 91b7dee into WordPress:trunk Jul 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Data Loss: Iframe Windows Close Without Unsaved Changes Warning

2 participants