Skip to content

Add ModalStack and DrawerStack #606

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 30, 2025

Conversation

AnnMarieW
Copy link
Collaborator

@AnnMarieW AnnMarieW commented Jun 13, 2025

Closes #603

This PR adds Modal Stack and Drawer Stack

Todo

@AnnMarieW
Copy link
Collaborator Author

Here's the example from the Mantine docs for ModalStack

from dash import Dash, Input, Output,  ctx, no_update
import dash_mantine_components as dmc

app = Dash()

component = dmc.Center([
    dmc.ModalStack(
        id="modal-stack",
        children=[
            dmc.ManagedModal(
                id={"index": "delete-page"},
             #   id="delete-page",
                title="Delete this page?",
                children=[
                    dmc.Text("Are you sure you want to delete this page? This action cannot be undone."),
                    dmc.Group(
                        mt="lg",
                        justify="flex-end",
                        children=[
                            dmc.Button("Cancel", id="cancel-1", variant="default"),
                            dmc.Button("Delete", id="delete", color="red"),
                        ],
                    ),
                ],
            ),
            dmc.ManagedModal(
                id="confirm-action",
                title="Confirm action",
                children=[
                    dmc.Text("Are you sure you want to perform this action? This action cannot be undone. If you are sure, press confirm button below."),
                    dmc.Group(
                        mt="lg",
                        justify="flex-end",
                        children=[
                            dmc.Button("Cancel", id="cancel-2", variant="default"),
                            dmc.Button("Confirm", id="confirm", color="red"),
                        ],
                    ),
                ],
            ),
            dmc.ManagedModal(
                id="really-confirm-action",
                title="Really confirm action",
                children=[
                    dmc.Text("Jokes aside. You have confirmed this action. This is your last chance to cancel it. After you press confirm button below, action will be performed and cannot be undone. For real this time. Are you sure you want to proceed?"),
                    dmc.Group(
                        mt="lg",
                        justify="flex-end",
                        children=[
                            dmc.Button("Cancel", id="cancel-3", variant="default"),
                            dmc.Button("Confirm", id="final-confirm", color="red"),
                        ],
                    ),
                ],
            ),
        ]
    ),
    dmc.Button("Open modal", id="open")
])

app.layout = dmc.MantineProvider([component])


@app.callback(
    Output("modal-stack", "open"),
    Output("modal-stack", "closeAll"),
    Input("open", "n_clicks"),
    Input("cancel-1", "n_clicks"),
    Input("cancel-2", "n_clicks"),
    Input("cancel-3", "n_clicks"),
    Input("delete", "n_clicks"),
    Input("confirm", "n_clicks"),
    Input("final-confirm", "n_clicks"),
    prevent_initial_call=True,
)
def control_modals(*_):
    match ctx.triggered_id:
        case "open":
            return {"index": "delete-page"}, False
        case "cancel-1" | "cancel-2" | "cancel-3" | "final-confirm":
            return None, True
        case "delete":
            return "confirm-action", False
        case "confirm":
            return "really-confirm-action", False
        case _:
            return no_update, no_update

if __name__ == "__main__":
    app.run(debug=True)

@AnnMarieW
Copy link
Collaborator Author

@alexcjohnson
This PR is still a draft, but when you get a chance, I’d love your thoughts on the API.

The ModalStack (and soon DrawerStack) components are a little different — the children are managed by a Mantine hook, and I’m exposing the hook methods (like open, closeAll, etc.) as props on the parent. Not sure if that’s the best approach.

I’ve got ModalStack working, and since DrawerStack will be similar, now’s a good time to make changes if needed. Appreciate any feedback!

@alexcjohnson
Copy link
Collaborator

Makes sense! It's a little annoying to have a new component type ManagedModal that you have to use with ModalStack, but I see why you did it this way, I don't see a better option.

If I were designing the API from the Mantine side I'm not sure why I'd want toggle - and at that point why make a closeAll? Just use open(null) or something like that. What do you think about supporting that usage pattern by triggering closeAll if you pass None to the open prop? That way your example would only need Output("modal-stack", "open"), to which you pass an ID or None. You could leave toggle and closeAll around as props for completeness with the Mantine API but recommend just using open alone.

@AnnMarieW
Copy link
Collaborator Author

I'd also like to avoid adding an additional ManagedModal component, but thought it would be easier to work with a separate component. I can also removed the opened prop to ensure that people use the ModelStack props to control the state. Is the name OK or do you think there is something better?

Great idea to support open(null). That will simplify dash callbacks. Thanks so much for the suggestion!

@alexcjohnson
Copy link
Collaborator

ManagedModal is good, the only alternative that occurs to me, if this is only ever going to be used inside a ModalStack, would be StackedModal, but would that confuse people about which is the inner vs outer component?

@AnnMarieW
Copy link
Collaborator Author

My dyslexic brain would never be able to keep StackedModal and ModalStack straight haha.

I’m struggling with using the open(null) to close all the models. It’s necessary to set the open prop back to null so if a model is closed by the user hitting the X or esc keys it can be reopened in a callback. I can make it work if the Dash callback uses open= “none” rather than open=None. But this could be confusing -- and maybe closing things with the open prop is not the best UI?

What do you think of just having one prop called stack and then access, open, close, closeAll and toggle functions in dict? {“open”: “modal-id”} Or {“closeAll” : True} etc... This would be similar to the Notifications API.

@alexcjohnson
Copy link
Collaborator

Hmph I thought this just supported one open modal at a time but maybe it allows multiple, otherwise why would state be a dict with an entry for each one? If that's the case, the way you have it is probably the best option.

@AnnMarieW AnnMarieW marked this pull request as ready for review June 19, 2025 23:00
@AnnMarieW AnnMarieW requested review from snehilvj and alexcjohnson and removed request for snehilvj June 23, 2025 15:55
@AnnMarieW
Copy link
Collaborator Author

Here are some notes about the refactor

@alexcjohnson suggested making this a wrapped component because DrawerStack and ModalStack were nearly identical. They now use a shared createStackComponent function to handle the shared logic.

Each one is wrapped in a typed component (DrawerStack, ModalStack) so Dash picks up the right docstrings for each component.

Copy link
Collaborator

@alexcjohnson alexcjohnson left a comment

Choose a reason for hiding this comment

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

💃 Very nice!

@AnnMarieW AnnMarieW merged commit ac8af63 into snehilvj:master Jun 30, 2025
1 check passed
@AnnMarieW AnnMarieW deleted the add-ModalStack-DrawerStack branch June 30, 2025 18:14
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.

[Feature Request] Modal Stack & Drawer Stack
2 participants