Back to Chromium

Web Modal (`components/web_modal`)

components/web_modal/README.md

152.0.7945.112.2 KB
Original Source

Web Modal (components/web_modal)

The web_modal component manages web-contents modal dialogs in Chromium—prompts and dialogs scoped to a specific content::WebContents rather than the entire browser window (e.g., HTTP basic authentication, print preview, specialized permission prompts like File System Access confirmations or Page Embedded Permission Control modals, WebAuthn/passkey dialogs, and device choosers).

Because components/web_modal is decoupled from concrete desktop UI frameworks (Views browser windows, auxiliary bubbles, WebUI pickers, or embedders like CEF), containers rely on WebContentsModalDialogManagerDelegate and WebContentsModalDialogHost to manage dialog hosting, bounds clipping, input blocking, and visibility lifecycle.


Contract for Supporting Web-Contents Modal Dialogs

Any UI container or window hosting a WebContents that may display web-modal dialogs must adhere to a strict four-part lifecycle contract:

  1. Manager Creation (CreateForWebContents): Immediately upon WebContents creation, instantiate the per-WebContents dialog manager (web_modal::WebContentsModalDialogManager::CreateForWebContents(web_contents)).
  2. Attachment Wiring (SetDelegate(delegate)): Synchronously assign the container's delegate (manager->SetDelegate(delegate)) when attaching a WebContents to a window or view, strictly before any layout or activation runs.
  3. Dialog Hosting (WebContentsModalDialogHost): Implement WebContentsModalDialogManagerDelegate::GetWebContentsModalDialogHost() to supply a valid WebContentsModalDialogHost (ModalDialogHost).
  4. Detachment & Teardown Unsetting (SetDelegate(nullptr)): Explicitly clear the delegate (manager->SetDelegate(nullptr)) before tab reparenting between windows, C++ ownership hand-off (std::move), or window teardown.

Requirement 1: Manager Creation (CreateForWebContents)

The Rule

Immediately upon WebContents creation (or during tab-helper initialization), the container must instantiate the per-WebContents dialog manager:

cpp
web_modal::WebContentsModalDialogManager::CreateForWebContents(web_contents);

Why It Is Required

WebContentsModalDialogManager is stored as a content::WebContentsUserData<WebContentsModalDialogManager> and is not automatically created by content::WebContents::Create().

When a web feature or page requests a web-modal prompt, call sites query:

cpp
web_modal::WebContentsModalDialogManager* manager =
    web_modal::WebContentsModalDialogManager::FromWebContents(web_contents);

If CreateForWebContents() was never called on that WebContents, FromWebContents() returns nullptr, causing prompt requests to silently drop or trigger null-pointer crashes.

Canonical Reference

  • TabHelpers::AttachTabHelpers(): Calls web_modal::WebContentsModalDialogManager::CreateForWebContents(web_contents) for standard browser tabs upon initialization.

Requirement 2: Attachment Wiring & Synchronous Timing (SetDelegate(delegate))

The Rule

When attaching a WebContents to a window or container view, the container must synchronously assign its delegate before any layout or tab activation occurs:

cpp
web_modal::WebContentsModalDialogManager::FromWebContents(web_contents)
    ->SetDelegate(delegate);

Why Strict Synchronous Timing Is Required

Assigning the delegate synchronously prior to layout or activation prevents two classes of errors:

  • Blocked Display or Creation Crashes (Initial Attachment): If a web feature requests a modal prompt before a container assigns its delegate, dialog creation either crashes immediately (when querying the unassigned delegate for a host) or leaves the requested prompt queued and invisible.
  • Mispositioning During Container Transitions (Tab Dragging / Reparenting): Mispositioning specifically occurs when reparenting a WebContents that already has an active dialog across windows. While detached between containers, an active dialog temporarily has no modal host. If the destination window defers delegate assignment until after its initial layout pass, open dialogs cannot query the new host's bounds during layout, leaving them unpositioned or anchored to stale geometry.

As explicitly documented in Chromium's Browser::SetAsDelegate():

cpp
// The modal dialog delegate must be set during SetAsDelegate (not via a
// separate TabStripModelObserver) to ensure it is wired before layout
// triggered by OnActiveTabChanged.

Canonical Reference


Requirement 3: Dialog Host Contract (WebContentsModalDialogHost)

The Rule

The delegate's GetWebContentsModalDialogHost(web_contents) method must return a valid web_modal::WebContentsModalDialogHost (implementing ModalDialogHost).

Host Implementation Requirements

Any object acting as a WebContentsModalDialogHost must guarantee five core behaviors:

  1. Parent Native View (GetHostView()): Must return the gfx::NativeView against which modal dialog widgets are parented and positioned.
  2. Bounds Constraining (GetMaximumDialogSize()): Must return the maximum bounding gfx::Size (typically matching the visible bounds of the WebContents container) so dialogs are clipped inside the web area or browser window.
  3. Dialog Origin Positioning (GetDialogPosition()): Must return the gfx::Point origin relative to GetHostView() where the dialog of the given gfx::Size should be placed.
  4. Position Observer Notification (OnPositionRequiresUpdate): When the container window or view moves or resizes, the host must iterate through registered ModalDialogHostObserver instances and call OnPositionRequiresUpdate() so open dialogs recalculate their origin.
  5. Destruction Safety Notification (OnHostDestroying): Before the host object is destroyed, it must call OnHostDestroying() on all registered observers. If omitted, active dialog managers or open dialog widgets will retain dangling pointers to the destroyed host view.

Canonical Reference


Requirement 4: Detachment & Teardown Unsetting (SetDelegate(nullptr))

The Rule

Before a WebContents is reparented between windows, transferred to another container via C++ ownership hand-off (std::move), or left outliving its container window, the container must explicitly clear the delegate:

cpp
web_modal::WebContentsModalDialogManager::FromWebContents(web_contents)
    ->SetDelegate(nullptr);

Circumstances & Why Unsetting Is Required

CircumstanceWhy SetDelegate(nullptr) Is CriticalCanonical Reference
1. Tab Detachment / Dragging (TabDetachedAtImpl)While a tab is reparented between windows, it temporarily has no parent container. Unsetting prevents incoming prompts or layout updates during the drag from mutating or querying the source window.Browser::TabDetachedAtImpl() calling SetAsDelegate(contents, false)
2. Ownership Hand-off (std::move)When a secondary flow (ProfilePickerSignInProvider, onboarding window) transfers a WebContents to a normal browser window, it must unset its delegate prior to hand-off so the manager does not retain a pointer to the old container view.ProfilePickerSignInProvider::FinishFlow() calling ResetWebContentsDelegates()
3. Container TeardownWebContentsModalDialogManager stores raw_ptr<Delegate> delegate_. If a container window closes while the WebContents outlives it or tears down asynchronously, failing to clear SetDelegate(nullptr) triggers dangling pointer crashes (DANGLING_PTR_DETECTED).DevToolsWindow::OnBrowserClosed()
4. Tab Replacement (OnTabReplacedAt)When swapping out an existing WebContents in a tab slot for a new one, unhook the delegate on the discarded WebContents before attaching the incoming one.Browser::OnTabReplacedAt()

Lifecycle & Page Navigations

Understanding how web-contents modal dialogs behave when the underlying WebContents navigates involves a clean division of responsibility between container hosts and dialog feature controllers:

  • Automatic Cross-Site Teardown (Containers): WebContentsModalDialogManager is a content::WebContentsObserver. When a primary main frame commits a cross-site (!SameDomainOrHost(...)) navigation, DidFinishNavigation() automatically notifies observers (OnWillCloseOnNavigation) and closes all active and queued child dialogs (CloseAllDialogs()). Container implementers do not need any navigation-handling code.
  • Automatic BackForwardCache (BFCache) Protection: If a primary main frame navigates while a web-modal dialog is open, the manager automatically disables BFCache (DisabledReasonId::kModalDialog).
  • Same-Origin / Same-Site Dismissal (Dialog Features): Because the manager only auto-closes dialogs on cross-site navigations, dialogs remain open across same-origin or same-site primary main frame navigations. Dialog feature controllers whose prompts should dismiss on any page navigation (e.g., PrintPreviewDialogController::DidFinishNavigation()) must observe WebContentsObserver directly and tear down their own prompt.
  • Pre-Teardown Navigation Hook: Dialog feature controllers needing to distinguish whether their prompt closed due to a user clicking "Cancel" versus being forcibly closed by an automatic cross-site navigation teardown (e.g., DigitalIdentitySafetyInterstitialControllerDesktop::CloseOnNavigationObserver) can implement WebContentsModalDialogManager::Observer::OnWillCloseOnNavigation().

Summary Checklist for Implementers

cpp
// 1. CREATION: Immediately after creating a WebContents
web_modal::WebContentsModalDialogManager::CreateForWebContents(web_contents);

// 2. ATTACHMENT: Synchronously when attaching to window/container (before layout)
web_modal::WebContentsModalDialogManager::FromWebContents(web_contents)
    ->SetDelegate(this);

// 3. DIALOG HOSTING: Delegate implementation supplies the modal dialog host
web_modal::WebContentsModalDialogHost* GetWebContentsModalDialogHost(
    content::WebContents* web_contents) override {
  return modal_dialog_host_;
}

// 4. DETACHMENT / TEARDOWN: Before detach, move, or window destruction
web_modal::WebContentsModalDialogManager::FromWebContents(web_contents)
    ->SetDelegate(nullptr);