streamlit / streamlit

Streamlit — A faster way to build and share data apps.
https://streamlit.io
Apache License 2.0
34.55k stars 3k forks source link

iframe height is being set to 0 in st.tabs in chrome - custom components do not render #7376

Open matthalstead opened 12 months ago

matthalstead commented 12 months ago

Checklist

Summary

I have been finding that some custom components (e.g. streamlit_folium, and my own react components) are not showing when they are added to any tab other than the first tab when using st.tab. I previously reported this here : https://github.com/randyzwitch/streamlit-folium/issues/128 - but have since found this affects some other components, i.e. not all components.

After looking at what is rendered in the html, the component html is certainly fine, but I notice that in the css style, the iframe of the component has been set to height 0. I am wondering how these heights get set and if there is some aspect of the components not advertising their rendered size or emitting a completed event that is causing this issue?

Note: this issue does not occur in Safari.

Reproducible Code Example

Open in Streamlit Cloud

import folium
from streamlit_folium import st_folium
import streamlit as st

st.title("Location stuff")
tab1, tab2 = st.tabs(["tab1", "tab2"])

with tab1:
    st.text('tab1')

with tab2:
    center = [-36.85605386574607, 174.75310922743114]
    m = folium.Map(location=center, zoom_start=10)
    st_folium(m, width=725)

Steps To Reproduce

run the streamlit app and select tab2

Expected Behavior

Expect the map to render, but it doesn't.

I get the following when inspecting the iframe with dev-tools

iframe[Attributes Style] {
    width: 651px;
    height: 0px;
}

If I switch it to rendering in tab1 I get

iframe[Attributes Style] {
    width: 645px;
    height: 720px;
}

Here is more detail in a screenshot

html_and_css_properties

Current Behavior

I have attached some screenshots.

  1. screenshot of when component is in tab2
  2. screenshot of when component is in tab1
  3. screenshot of when component is in tab2 and height of iframe set to auto or 100%
  4. screenshot of when component is in tab2 and height of iframe set to 1000px
component_in_tab_2 component_in_tab_1 iframe_height_auto_and_100percent iframe_height_1000px

Is this a regression?

Debug info

Additional Information

No response

github-actions[bot] commented 12 months ago

If this issue affects you, please react with a 👍 (thumbs up emoji) to the initial post.

Your feedback helps us prioritize which bugs to investigate and address first.

Visits

matthalstead commented 12 months ago

I did some more tests on my own custom component. It looks like when we first render the page with st.tabs in it, the component in the non visible tab gets a document.body.scrollHeight of 0. When you switch to the tab, we get a componentDidUpdate event and the scroll height is non zero. If I set the frame height at this point, the component is now the correct height and renders.

I also include testing all the various heights and choose the max for now. This is just for demonstration purposes.

  public componentDidMount() {
    super.componentDidMount.apply(this);
    const scrollHeight = Math.max(
      document.body.scrollHeight, document.documentElement.scrollHeight,
      document.body.offsetHeight, document.documentElement.offsetHeight,
      document.body.clientHeight, document.documentElement.clientHeight
    );
    console.log("component mounted")
    console.log("scroll height is :" + scrollHeight)
    console.log("document.body.scrollHeight : " + document.body.scrollHeight)
    //Streamlit.setFrameHeight(document.body.scrollHeight)
    Streamlit.setFrameHeight(scrollHeight)
  }

  public componentDidUpdate(): void {
    super.componentDidUpdate.apply(this);
    const scrollHeight = Math.max(
      document.body.scrollHeight, document.documentElement.scrollHeight,
      document.body.offsetHeight, document.documentElement.offsetHeight,
      document.body.clientHeight, document.documentElement.clientHeight
    );
    console.log("component updated")
    console.log("scroll height is :" + scrollHeight)
    console.log("document.body.scrollHeight : " + document.body.scrollHeight)
    Streamlit.setFrameHeight(scrollHeight)
  }

When app opens:

component mounted
scroll height is :0
document.body.scrollHeight : 0

When switching to the tab with the component in it

component updated
scroll height is :472
document.body.scrollHeight : 456
gmatas-edison commented 6 months ago

I found a workaround that may work for you:

Function to inject JS code within an iFrame

def inject_iframe_js_code(source: str) -> None:
    div_id = uuid.uuid4()

    st.markdown(
        f"""
    <div style="height: 0; width: 0; overflow: hidden;" id="{div_id}">
        <iframe src="javascript: \
            var script = document.createElement('script'); \
            script.type = 'text/javascript'; \
            script.text = {html.escape(repr(source))}; \
            var div = window.parent.document.getElementById('{div_id}'); \
            div.appendChild(script); \
            setTimeout(function() {{ }}, 0); \
        "></iframe>
    </div>
    """,
        unsafe_allow_html=True,
    )

Function to check for a specific iFrame and set its height

    def js_show_zeroheight_iframe(component_iframe_title: str, height: int = 600):
    source = f"""
    (function() {{
        var attempts = 0;
        const maxAttempts = 20; // Max attempts to find the iframe
        const intervalMs = 1000; // Interval between attempts in milliseconds

        const intervalId = setInterval(function() {{
            var iframe = document.querySelector('iframe[title="{component_iframe_title}"]');
            if (iframe || attempts > maxAttempts) {{
                if (iframe) {{
                    iframe.style.height = "{height}px";
                    iframe.setAttribute("height", "{height}");
                    console.log('Height of iframe with title "{component_iframe_title}" set to {height}px.');
                }} else {{
                    console.log('Iframe with title "{component_iframe_title}" not found after ' + maxAttempts + ' attempts.');
                }}
                clearInterval(intervalId); // Stop checking
            }}
            attempts++;
        }}, intervalMs);
    }})();
    """

    inject_iframe_js_code(source)

Call example

# Return selection
    return_select = tree_select(
        tree,
        show_expand_all=True,
        check_model=return_mode,
        checked=pre_selected_nodes,
        key=key,
    )

    # Apply CSS Styling to avoid zero height issue (`streamlit v1.32.2`)
    js_show_zeroheight_iframe(
        component_iframe_title="streamlit_tree_select.streamlit_tree_select",
        height=600,
    )
map0logo commented 4 months ago

In my case it happens that the cookie manager of https://github.com/Mohamed-512/Extra-Streamlit-Components, with the purpose of hiding itself, makes the height of the iframe 0. But it does it in such a way that all the iframes of the page are made of zero height, unless the other components take preventive measures before this situation.

Using the cookie manager at the top of the page caused the iframe of an instance of the https://github.com/ChrisDelClea/streamlit-agraph component not to be displayed, i.e. it was published inside a zero-height iframe.

I tried the workaround recommended by @gmatas-edison, but the problem is that when the network diagram is initialized, the height of the canvas is zero. And although the height is effectively changed later, the network diagram appears in the wrong position, and it becomes complicated to go inside the component to adjust vis.js.

I commented the lines related to the cookie manager and voila! The network diagram appeared perfectly centered. I don't know how it is in every case, but in my case it happened as a result of an interaction between components. So either the streamlit components need to be redesigned so that they do not interfere with each other in unpredictable ways, or a best practices guide needs to be developed so that unexpected interferences do not occur.

So, @matthalstead, I recommend you check other components you use in the same page. In particular, components that are inside a zero-height iframe.

map0logo commented 4 months ago

Maybe the streamlit-agraph component only needs to use the Streamlit.setFrameHeight method.

gmatas-edison commented 3 months ago

I found a workaround that may work for you:

Function to inject JS code within an iFrame

def inject_iframe_js_code(source: str) -> None:
    div_id = uuid.uuid4()

    st.markdown(
        f"""
    <div style="height: 0; width: 0; overflow: hidden;" id="{div_id}">
        <iframe src="javascript: \
            var script = document.createElement('script'); \
            script.type = 'text/javascript'; \
            script.text = {html.escape(repr(source))}; \
            var div = window.parent.document.getElementById('{div_id}'); \
            div.appendChild(script); \
            setTimeout(function() {{ }}, 0); \
        "></iframe>
    </div>
    """,
        unsafe_allow_html=True,
    )

Call example

# Return selection
    return_select = tree_select(
        tree,
        show_expand_all=True,
        check_model=return_mode,
        checked=pre_selected_nodes,
        key=key,
    )

    # Apply CSS Styling to avoid zero height issue (`streamlit v1.32.2`)
    js_show_zeroheight_iframe(
        component_iframe_title="streamlit_tree_select.streamlit_tree_select",
        height=600,
    )

I have updated the js_show_zeroheight_iframe to work only on hidden iframes. It is now also activated by 'click' events:

def js_show_zeroheight_iframe(component_iframe_title: str, height: str = "auto"):
    source = f"""
    (function() {{
    var attempts = 0;
    const maxAttempts = 20; // Max attempts to find the iframe
    const intervalMs = 250; // Interval between attempts in milliseconds

    function setIframeHeight() {{
        const intervalId = setInterval(function() {{
            var iframes = document.querySelectorAll('iframe[title="{component_iframe_title}"]');
            if (iframes.length > 0 || attempts > maxAttempts) {{
                if (iframes.length > 0) {{
                    iframes.forEach(iframe => {{
                        if (iframe || iframe.height === "0" || iframe.style.height === "0px") {{
                            iframe.style.height = "{height}";
                            iframe.setAttribute("height", "{height}");
                            console.log('Height of iframe with title "{component_iframe_title}" set to {height}.');
                        }}
                    }});
                }} else {{
                    console.log('Iframes with title "{component_iframe_title}" not found after ' + maxAttempts + ' attempts.');
                }}
                clearInterval(intervalId); // Stop checking
            }}
            attempts++;
        }}, intervalMs);
    }}

    function trackInteraction(event) {{
        console.log('User interaction detected:', event.type);
        setIframeHeight();
    }}

    setIframeHeight();
    document.addEventListener('click', trackInteraction);
}})();
    """

    inject_iframe_js_code(source)