Dash 4 and html2pdf bug with radioitems

I have a small app that can create a screenshot of the webpage and save it as a pdf report. It uses a clientside callback and the html2pdf library to create the screenshot. Starting at Dash 4.0 there is a buggy interaction where dcc.RadioItems components have the current selection deselected when a screenshot is generated. I did some backwards testing with Dash 2.18 and 3.3 and the issue didn’t occur there.

To try out, load the included sample app and press the pdf report button. The screenshot generates. Then check the state of the radio items component in the dashboard. With Dash 2.18 and Dash 3.3 the last selected item is still selected. Now install Dash 4.0, after generating the screenshot, the radioitems selection gets unselected.

I looked in the html2pdf repo for issues on this and found 2 issues: issue 1 and issue 2, dating from 2019, but those suggestions don’t really help.

Any here that have some experience with html2pdf that have an idea what’s going on? Or any Dash 4 experts have an idea what change in the dcc library triggered this behavior?

Edit: I just saw that html2pdf has version 0.14.0 available from the Github page and I was still referring to the old 0.10.1 release. The issue still occurs with the latest version of html2pdf, so that also doesn’t resolve the issue. Unfortunately, the latest release is not available from CDN so to test the latest release you have to download the release and put the bundle file in the assets folder next to your app. If you do, you can remove the app.index_string statement from the sample app.

Requirements

# Interaction ok
dash==2.18.1
plotly==5.24.0
dash-bootstrap-components==1.6.0
# Interaction ok!
dash==3.3.0
plotly==6.6.0
dash-bootstrap-components==2.0.4
# Interaction bad :(
dash==4.0.0
plotly==6.6.0
dash-bootstrap-components==2.0.4

Sample app

import dash
from dash import dcc, html, clientside_callback, Input
import dash_bootstrap_components as dbc

app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

app.index_string = """
<!DOCTYPE html>
<html>
    <head>
        {%metas%}
        <title>{%title%}</title>
        {%favicon%}
        {%css%}
    </head>
    <body>
        {%app_entry%}
        <footer>
            {%config%}
            {%scripts%}
            <script type="text/javascript" 
                src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js" 
                integrity="sha512-GsLlZN/3F2ErC5ifS5QtgpiJtWd43JWSuIgh7mbzZ8zBps+dvLusV+eNQATqgA/HdeKFVgA5v3S/cIrLF7QnIg==" 
                crossorigin="anonymous" 
                referrerpolicy="no-referrer">
            </script>
            {%renderer%}
        </footer>
    </body>
</html>
"""

button_pdf_report = dbc.Button(
    id="button_download_report",
    children="Download PDF report",
    color="primary",
    outline=True,
    style={"marginLeft": "10px", "width": "200px"},
)

app.layout = dbc.Container(
    [
        html.H2("Radio Items & PDF Report", className="my-4"),
        dcc.RadioItems(
            id="radio-items",
            options=[
                {"label": "Option A", "value": "A"},
                {"label": "Option B", "value": "B"},
                {"label": "Option C", "value": "C"},
            ],
            value="A",
            labelStyle={"display": "block", "marginBottom": "8px"},
        ),
        html.Div(button_pdf_report, className="mt-4"),
    ],
    className="p-4",
    id="main_container",
    style={"minWidth": "1253px"}
)

clientside_callback(
    """
    function (button_clicked) {
        if (button_clicked && button_clicked > 0) {
            var mainContainerElement = document.getElementById("main_container");
            var main_container_width = parseInt(mainContainerElement.style.minWidth);

            var opt = {
                // margin units are those that are defined in jsPDF key below.
                margin: 10,
                filename: "test_file.pdf",
                image: { type: 'jpeg', quality: 0.98 },
                html2canvas: {
                    scale: 3,
                    width: main_container_width,
                    dpi: 300,
                },
                jsPDF: { unit: 'mm', format: 'A4', orientation: 'p' },
            };
            html2pdf().from(mainContainerElement).set(opt).save();
        }
    }
    """,
    Input(component_id="button_download_report", component_property="n_clicks"),
)

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

I did some further debugging on the sample app and what I see is that the html of the RadioItems is not even altered by the html2pdf renderer. In the raw html, the item that was selected, remains selected, as can be seen from the aria-selected="true" in the raw html. So it seems just the visual of the selection isn’t (re)rendered properly after the screenshot.

Hello @Tobs

More than likely, I suspect that this could be due to some css settings somewhere, if you can find it, you might be able to print it.

You could also try to make sure that background images are turned on when printing the pdf.

I reviewed what I could in the css in the browser but I couldn’t find any difference there. If it’s inside the Dash 4 css files, then that would be some task to review.

I did try to let the AI analyze it issue as well, but it wasn’t able to make much out of it. The only direction that might make sense to me is related to a comment made in one of the GitHub issues: for me the issue was a "name" attribute of radio button

When I look at the html of RadioItems in Dash 4 vs Dash 3 then I see that in Dash 4 the RadioItems has a name attribute. (The raw html is attached below). In one of the AI analyses it says:

When html2canvas clones this input and inserts the clone into the document (even temporarily), the browser sees two <input type="radio"> elements with the same name. Checking/unchecking one in the clone can deselect the original, because radio buttons with the same name are in the same group browser-wide.

So it seems to hint that since the radio items all have the same name, it conflicts on the rerender of the page. Does this reasoning make any sense?

Dash 4

<div id="radio-items" class="dash-options-list dash-radioitems " role="listbox">
    <label class="dash-options-list-option selected" role="option" aria-selected="true">
        <span class="dash-options-list-option-wrapper">
            <input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="A" checked="">
        </span>
        <span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
            <span>Option A</span>
        </span>
    </label>
    <label class="dash-options-list-option" role="option" aria-selected="false">
        <span class="dash-options-list-option-wrapper">
            <input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="B">
        </span>
        <span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
            <span>Option B</span>
        </span>
    </label>
    <label class="dash-options-list-option" role="option" aria-selected="false">
        <span class="dash-options-list-option-wrapper">
            <input type="radio" name="radio-items" readonly="" class="dash-options-list-option-checkbox" value="C">
        </span>
        <span class="dash-options-list-option-text" style="display: block; margin-bottom: 8px;">
            <span>Option C</span>
        </span>
    </label>
</div>

Dash 3

<div id="radio-items">
    <label style="display: block; margin-bottom: 8px;" class="">
        <input class="" type="radio" checked="">Option A
    </label>
    <label style="display: block; margin-bottom: 8px;" class="">
        <input class="" type="radio">Option B
    </label>
    <label style="display: block; margin-bottom: 8px;" class="">
        <input class="" type="radio">Option C
    </label>
</div>

It works when I take the raw html and place it into codepen:

1 Like

Indeed, that is the key point. The raw html is correct, the css styling what I could see in the browser dev console is correct. Both show that Option A is selected. The problem is that after html2pdf is done creating the pdf screenshot, the dash app doesn’t visualize that the option is selected.

What I understood from the AI about the html2pdf library is that it takes a clone of the webpage and then renders the screenshot. The web page is then rerendered? It seems that with the changes to the dcc components in Dash 4 something in that process is creating a conflict. The AI was hinting to the name attribute as the root cause (since they are the same for all radio items) but honestly, it’s outside my knowledge area to assess whether that makes sense. So I am hoping to get more insights on where the root cause might lie.