Skip to content

feat(Modal): add OnClosing parameter#7632

Merged
ArgoZhang merged 11 commits intomainfrom
feat-dialog
Feb 5, 2026
Merged

feat(Modal): add OnClosing parameter#7632
ArgoZhang merged 11 commits intomainfrom
feat-dialog

Conversation

@ArgoZhang
Copy link
Member

@ArgoZhang ArgoZhang commented Feb 5, 2026

Link issues

fixes #7631

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Add support for asynchronously vetoing modal closure and wire it through UI and JavaScript interactions.

New Features:

  • Introduce an OnClosingAsync callback on Modal that allows consumers to determine asynchronously whether the modal should close.

Enhancements:

  • Add JS-invokable BeforeCloseCallback and integrate it into ESC, backdrop, and programmatic close flows to centralize pre-close checks.
  • Expose registration and unregistration helpers for modal closing callbacks.
  • Tidy up ModalDialog and Dialog markup and simplify Dialog internal callback fields.

Copilot AI review requested due to automatic review settings February 5, 2026 10:51
@bb-auto bb-auto bot added the enhancement New feature or request label Feb 5, 2026
@bb-auto bb-auto bot added this to the v10.3.0 milestone Feb 5, 2026
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 5, 2026

Reviewer's Guide

Adds a pre-close callback pipeline to Modal so callers can asynchronously decide whether a modal is allowed to close, wires this into the JS bootstrap modal behavior, and does minor cleanup/formatting around Modal, ModalDialog, and Dialog components.

Sequence diagram for the new modal pre-close (OnClosingAsync) pipeline

sequenceDiagram
    actor User
    participant BrowserJS as Browser_JS_modal
    participant BlazorJS as Blazor_JSInterop
    participant Modal as Modal_component
    participant OnClosing as OnClosingAsync_callback

    User->>BrowserJS: Trigger close (Escape, backdrop click, or close button)
    BrowserJS->>BrowserJS: modal.close()

    BrowserJS->>BlazorJS: invokeMethodAsync BeforeCloseCallback
    BlazorJS->>Modal: BeforeCloseCallback()
    activate Modal
    alt OnClosingAsync is assigned
        Modal->>OnClosing: Invoke OnClosingAsync()
        activate OnClosing
        OnClosing-->>Modal: Task<bool> result
        deactivate OnClosing
    else OnClosingAsync is null
        Modal-->>BlazorJS: default true
    end
    Modal-->>BlazorJS: bool allowClose
    deactivate Modal

    BlazorJS-->>BrowserJS: bool allowClose

    alt allowClose is true
        BrowserJS->>BrowserJS: modal.hide()
        BrowserJS->>BlazorJS: invokeMethodAsync CloseCallback
        BlazorJS->>Modal: CloseCallback()
        Modal-->>Modal: Execute OnCloseAsync if assigned
    else allowClose is false
        BrowserJS-->>User: Modal remains open
    end
Loading

Updated class diagram for Modal pre-close callback support

classDiagram
    class Modal {
        +Func~Task~ OnCloseAsync
        +Func~Task~bool~~ OnClosingAsync
        +Task Close()
        +Task~bool~ BeforeCloseCallback()
        +void RegisterOnClosingCallback(Func~Task~bool~~ onClosingCallback)
        +void UnRegisterOnClosingCallback(Func~Task~bool~~ onClosingCallback)
    }
Loading

File-Level Changes

Change Details Files
Introduce an async pre-close callback API on Modal and surface registration helpers.
  • Add OnClosingAsync parameter of type Func<Task>? with XML docs (zh/en).
  • Add JS-invokable BeforeCloseCallback that evaluates OnClosingAsync and returns a bool to JS.
  • Change Close() to async, invoking BeforeCloseCallback and only hiding the modal when it returns true.
  • Add RegisterOnClosingCallback / UnRegisterOnClosingCallback helpers that compose OnClosingAsync via += and -=.
src/BootstrapBlazor/Components/Modal/Modal.razor.cs
Update JS modal behavior to consult the pre-close callback before hiding on keyboard or backdrop interactions.
  • Normalize import line encoding in Modal.razor.js.
  • Add modal.close method that calls .NET BeforeCloseCallback via invokeMethodAsync and hides only when it returns true.
  • Update Escape-key handler to be async and delegate to modal.close instead of directly hiding.
  • Update backdrop click handler to delegate to modal.close instead of directly hiding.
src/BootstrapBlazor/Components/Modal/Modal.razor.js
Minor markup/field cleanup in ModalDialog and Dialog components.
  • Reformat Button/PrintButton elements in ModalDialog for multi-line attribute layout without behavioral changes.
  • Slightly reorder Modal attributes in Dialog.razor, using direct attribute binding instead of @ for some fields.
  • Remove unnecessary [NotNull] annotations from private delegate fields in Dialog.razor.cs while keeping behavior unchanged.
src/BootstrapBlazor/Components/Modal/ModalDialog.razor
src/BootstrapBlazor/Components/Dialog/Dialog.razor
src/BootstrapBlazor/Components/Dialog/Dialog.razor.cs
src/BootstrapBlazor/BootstrapBlazor.csproj

Assessment against linked issues

Issue Objective Addressed Explanation
#7631 Add a pre-close callback parameter to the Modal component that allows consumers to run logic before closing and optionally prevent the Modal from closing.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new RegisterOnClosingCallback/UnRegisterOnClosingCallback APIs layer multiple Func<Task<bool>> onto OnClosingAsync via +=/-=, but the current BeforeCloseCallback invocation only uses the final delegate’s bool result; if multiple callbacks are expected, consider explicitly aggregating the invocation list (e.g., AND/OR semantics) or restricting OnClosingAsync to a single handler to avoid surprising behavior.
  • Both the JS-side modal.close() and the C# Close() method now call BeforeCloseCallback, but they do so independently; it may be worth centralizing the pre-close check into a single code path (either always via JS or always via C#) to ensure all close flows behave identically and avoid future divergence.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `RegisterOnClosingCallback`/`UnRegisterOnClosingCallback` APIs layer multiple `Func<Task<bool>>` onto `OnClosingAsync` via `+=`/`-=`, but the current `BeforeCloseCallback` invocation only uses the final delegate’s `bool` result; if multiple callbacks are expected, consider explicitly aggregating the invocation list (e.g., AND/OR semantics) or restricting `OnClosingAsync` to a single handler to avoid surprising behavior.
- Both the JS-side `modal.close()` and the C# `Close()` method now call `BeforeCloseCallback`, but they do so independently; it may be worth centralizing the pre-close check into a single code path (either always via JS or always via C#) to ensure all close flows behave identically and avoid future divergence.

## Individual Comments

### Comment 1
<location> `src/BootstrapBlazor/Components/Modal/Modal.razor.cs:91` </location>
<code_context>
+    /// <para lang="en">Callback Method Before Closing. Return true to close, false to prevent closing</para>
+    /// </summary>
+    [Parameter]
+    public Func<Task<bool>>? OnClosingAsync { get; set; }
+
     /// <summary>
</code_context>

<issue_to_address>
**issue (bug_risk):** Using a multicast Func<Task<bool>> means only the last registered callback's result is honored.

Because this is a `Func<Task<bool>>` property that callers extend with `+=`/`-=`, it becomes a multicast delegate. When invoked, only the result of the last registered callback is used. If multiple callbacks are expected to collectively determine whether closing is allowed, this won’t work as intended. Instead, keep an internal collection of callbacks and explicitly combine their results (e.g., logical AND over all returned values).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov
Copy link

codecov bot commented Feb 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (eb6dad4) to head (df9a754).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #7632   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          749       749           
  Lines        33017     33040   +23     
  Branches      4581      4583    +2     
=========================================
+ Hits         33017     33040   +23     
Flag Coverage Δ
BB 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds an OnClosingAsync parameter to the Modal component, allowing consumers to intercept and potentially prevent modal closure by returning false from the callback. This callback is invoked before the modal closes in response to user interactions such as pressing the Escape key, clicking the backdrop, or clicking close buttons.

Changes:

  • Added OnClosingAsync parameter to Modal component that returns a boolean to allow or prevent closing
  • Modified JavaScript handlers for Escape key and backdrop clicks to check the callback before closing
  • Updated Modal.Close() method to invoke the callback before hiding the modal
  • Added RegisterOnClosingCallback/UnRegisterOnClosingCallback methods for programmatic callback management

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/BootstrapBlazor/Components/Modal/Modal.razor.cs Added OnClosingAsync parameter, BeforeCloseCallback JSInvokable method, updated Close() method, and added registration methods
src/BootstrapBlazor/Components/Modal/Modal.razor.js Added modal.close() function that checks BeforeCloseCallback; updated Escape and backdrop handlers to use it
src/BootstrapBlazor/Components/Modal/ModalDialog.razor Formatting changes only (attribute line breaks)
src/BootstrapBlazor/Components/Dialog/Dialog.razor.cs Removed incorrect [NotNull] attributes from nullable fields
src/BootstrapBlazor/Components/Dialog/Dialog.razor Formatting changes only (attribute organization)
src/BootstrapBlazor/BootstrapBlazor.csproj Version bump from 10.3.1-beta06 to 10.3.1

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +290 to +293
public void RegisterOnClosingCallback(Func<Task<bool>> onClosingCallback)
{
OnClosingAsync += onClosingCallback;
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using delegate composition with OnClosingAsync += onClosingCallback, C# multicast delegates will only return the result of the last delegate in the invocation list. If multiple callbacks are registered using RegisterOnClosingCallback, only the last registered callback's return value will determine whether the modal closes. All callbacks will execute, but intermediate return values are discarded. This could lead to unexpected behavior where an earlier callback returns false to prevent closing, but a later callback returns true and allows it. Consider using a cache-based approach similar to _shownCallbackCache where you can iterate through all callbacks and properly aggregate their results (e.g., close only if all callbacks return true).

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +92
/// <summary>
/// <para lang="zh">关闭之前回调方法 返回 true 时关闭弹窗 返回 false 时阻止关闭弹窗</para>
/// <para lang="en">Callback Method Before Closing. Return true to close, false to prevent closing</para>
/// </summary>
[Parameter]
public Func<Task<bool>>? OnClosingAsync { get; set; }

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new OnClosingAsync parameter and related functionality lack test coverage. The existing test file has comprehensive tests for other callbacks like OnShownAsync (see ShownCallbackAsync_Ok test) and RegisterShownCallback (see RegisterShownCallback_Ok test). Consider adding tests to verify: (1) OnClosingAsync is invoked when closing the modal, (2) returning false prevents the modal from closing, (3) returning true allows the modal to close, (4) the callback is invoked for all close triggers (escape key, backdrop click, close button), and (5) RegisterOnClosingCallback and UnRegisterOnClosingCallback work correctly.

Copilot uses AI. Check for mistakes.
@ArgoZhang ArgoZhang merged commit ee579b4 into main Feb 5, 2026
6 checks passed
@ArgoZhang ArgoZhang deleted the feat-dialog branch February 5, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(Modal): add OnClosing parameter

1 participant