plotly / dash

Data Apps & Dashboards for Python. No JavaScript Required.

Home Page:https://plotly.com/dash

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[BUG] dcc.Clipboard functionality broken in Notification component

DavidKatz-il opened this issue · comments

The following code creates a notification using dash_mantine_components.Notification, with a dcc.Clipboard as its icon.
Since dash version 2.15.0, the clipboard functionality appears to be broken.
It seems that this issue is related to the following PR #2652.

from dash import Dash, html, callback, Output, Input, dcc, dash
import dash_mantine_components as dmc


@callback(
    Output("notifications-container", "children"),
    Input("notify", "n_clicks"),
    prevent_initial_call=True,
)
def notify(n_clicks):
    return dmc.Notification(
        title="Notification",
        autoClose=False,
        id="simple-notify",
        action="show",
        message=f"{n_clicks=}",
        icon=dcc.Clipboard(title="copy", content="icon text"),
    )


app = Dash(__name__)
app.layout = dmc.NotificationsProvider(
    html.Div(
        [
            dmc.Text(children=dash.__version__),
            dmc.Button("Show Notification", id="notify"),
            html.Div(id="notifications-container"),
            html.Div(id="container"),
        ]
    )
)

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

pip list | grep dash

Works with:
dash                    2.14.0
dash-core-components    2.0.0
dash-html-components    2.0.0
dash-iconify            0.1.2
dash-mantine-components 0.12.1
dash-pivottable         0.0.2
dash-table              5.0.0
dash-testing-stub       0.0.2

Doesn't work with:
dash                    2.15.0
dash-core-components    2.0.0
dash-html-components    2.0.0
dash-iconify            0.1.2
dash-mantine-components 0.12.1
dash-pivottable         0.0.2
dash-table              5.0.0
dash-testing-stub       0.0.2

Is this issue related to dash or to dash_mantine_components?

Hi @DavidKatz-il

Thanks for reporting. I could replicate this with your example. Given that it's using the same version of dmc, it's reasonable to assume that it's caused by something that changed between Dash versions 2.14 and 2.15.

I'm not sure what is causing the issue though. To narrow things down, I checked to see if it was because of dcc.Clipboard being returned in a callback or used to set a prop. This works in both Dash versions -- the callback returns a a dcc.Checklist rather than the dmc.Notification:

@callback(
    Output("notifications-container", "children"),
    Input("notify", "n_clicks"),
    prevent_initial_call=True,
)
def notify(n_clicks):
    return dcc.Checklist(
        [
            {
                "label": [
                    html.Span(dcc.Clipboard(title="copy", content="icon text", id="id1")),
                    html.Span("Python", style={"font-size": 15, "padding-left": 10}),
                ],
                "value": "Python",
            },
        ], labelStyle={"display": "flex", "align-items": "center"}
    )

Not sure what in the dmc.Notification is conflicting. Do you have any ideas?

Hi @AnnMarieW, Thanks for the quick response.

I noticed that it works fine with Dash callbacks.
Upon debugging in the browser, I found that in the case of dmc.Notification, this.props.n_clicks always remains 0. Consequently, it triggers this code block:

        if (
            !this.props.n_clicks ||
            this.props.n_clicks === prevProps.n_clicks
        ) {
            return;
        }

This behavior seems to have been added in PR #2652.

That’s helpful - thanks @DavidKatz-il !

The question is why is it not registering the n_clicks when it's in the dmc.Notification. The n_clicks works fine when it's returned in the dcc.Checklist 🤔

Here's another minimal app to narrow down the issue. In this one, I've replaced the dcc.Clipboard with an html.Button If you click on the button in the notification, you will see that the callback is triggered but n_clicks remains on 1 even after clicking on the button multiple times. Given that n_clicks doesn't change then the new code in dcc.Clipboard as mentioned above makes it so the clipboard doesn't work.

So maybe it's a dmc issue? It might be due to it being in the icon prop.

from dash import Dash, html, callback, Output, Input, dcc, dash
import dash_mantine_components as dmc

app = Dash(__name__, suppress_callback_exceptions=True)

app.layout = dmc.NotificationsProvider(
    html.Div(
        [
            dmc.Text(children=dash.__version__),
            dmc.Button("Show Notification", id="notify"),
            html.Div(id="notifications-container"),
            html.Div(id="container"),
        ]
    ),
)


@callback(
    Output("notifications-container", "children"),
    Input("notify", "n_clicks"),
    prevent_initial_call=True,
)
def notify(n_clicks):
    return dmc.Notification(
        title="Notification",
        autoClose=False,
        id="simple-notify",
        action="show",
        message=f"{n_clicks=}",
        icon=html.Button("click me", id="button"),
    )


@callback(
    Output("container", "children"),
    Input("button", "n_clicks")
)
def update(n):
    print("button n_clicks:", n)
    return dash.no_update


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

Additionally, the same issue occurred with the message prop.

from dash import Dash, html, callback, Output, Input, dcc, dash
import dash_mantine_components as dmc


@callback(
    Output("notifications-container", "children"),
    Input("notify", "n_clicks"),
    prevent_initial_call=True,
)
def notify(n_clicks):
    return dmc.Notification(
        title="Notification",
        autoClose=False,
        id="simple-notify",
        action="show",
        message=dcc.Clipboard(title="copy", content="message text"),
    )


app = Dash(__name__)
app.layout = dmc.NotificationsProvider(
    html.Div(
        [
            dmc.Text(children=dash.__version__),
            dmc.Button("Show Notification", id="notify"),
            html.Div(id="notifications-container"),
            html.Div(id="container"),
        ]
    )
)

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

So maybe it's a dmc issue?

Interesting, dmc must be doing something with the props and never receive the new n_clicks value in the onClick handler.