Skip to content
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

rx.tooltip doesn't work with rx.button when that rx.button has additional properties #3278

Open
greenseeker opened this issue May 12, 2024 · 14 comments
Labels
bug Something isn't working

Comments

@greenseeker
Copy link

Describe the bug
Tooltips are not displayed for (icon_)buttons once that button has addition properties specified.

To Reproduce
Use the following code in a page. Hovering over the icon_button will not display the tooltip.

rx.tooltip(
    rx.icon_button(
        "home", 
        on_click=rx.redirect("/"), 
        disabled=(AppState.router.page.path=="/")
    ), 
    content="Go to homepage"
)

If you comment out the on_click and disabled lines, then the tooltip works.

rx.tooltip(
    rx.icon_button(
        "home", 
#        on_click=rx.redirect("/"), 
#        disabled=(AppState.router.page.path=="/")
    ), 
    content="Go to homepage"
)

Expected behavior
The tooltip should still be displayed.

Specifics (please complete the following information):

  • Python Version: 3.11.6
  • Reflex Version: 0.4.9
  • OS: Ubuntu 23.10
  • Browser (Optional): Brave 1.65.126, Firefox 125
@Lendemor Lendemor added the bug Something isn't working label May 13, 2024
@Lendemor
Copy link
Collaborator

Lendemor commented May 13, 2024

Thanks for the report.

Seems to be caused by the level of isolation we add when components need to use State related stuff.
With

rx.tooltip(
    rx.icon_button(
        "home", 
        disabled=True
    ), 
    content="Go to homepage"
)

the tooltip will show up properly.

Need to investigate on Radix side, it might be a bug from them.

@Lendemor
Copy link
Collaborator

Lendemor commented May 13, 2024

Trimmed down code that reproduce the issue :

/** @jsxImportSource @emotion/react */

import { Fragment } from "react";
import {
  Box as RadixThemesBox,
  Tooltip as RadixThemesTooltip,
} from "@radix-ui/themes";

export function Box_f821d43fc5d8628e6aa9573e5ba02f7f() {
  return <RadixThemesBox>{`home`}</RadixThemesBox>;
}

export default function Component() {
  return (
    <Fragment>
      <RadixThemesTooltip content={`Go to homepage`}>
        <Box_f821d43fc5d8628e6aa9573e5ba02f7f />
      </RadixThemesTooltip>
    </Fragment>
  );
}

Moving component directly in the same function where Tooltip is used make the code work:

/** @jsxImportSource @emotion/react */

import { Fragment } from "react";
import {
  Box as RadixThemesBox,
  Tooltip as RadixThemesTooltip,
} from "@radix-ui/themes";

export default function Component() {
  return (
    <Fragment>
      <RadixThemesTooltip content={`Go to homepage`}>
        <RadixThemesBox>{`home`}</RadixThemesBox>
      </RadixThemesTooltip>
    </Fragment>
  );
}

@greenseeker
Copy link
Author

After posting I went looking for a workaround with different components, but it seems to impact anything that would appear on hover, not just rx.tooltip.

@greenseeker
Copy link
Author

If there's any workaround for this, please let me know, even if it's hacky. My app is just a mockup, so I'm not worried about it being done "right". Even if there's some way to set the alt property and use default browser tooltips, that'd be good.

@masenf
Copy link
Collaborator

masenf commented May 13, 2024

If there's any workaround for this, please let me know

from reflex.components.component import MemoizationLeaf
from reflex.components.base.fragment import Fragment

class IntegralFragment(Fragment, MemoizationLeaf):
    pass

...

            IntegralFragment.create(
                rx.tooltip(
                    rx.icon_button(
                        "home", 
                        on_click=rx.redirect("/"), 
                        disabled=(State.router.page.path=="/")
                    ), 
                    content="Go to homepage"
                ),
            ),

Don't ask me why this works, needs further investigation. But it does appear to workaround the problem.

@masenf
Copy link
Collaborator

masenf commented May 14, 2024

Okay this actually helped a lot radix-ui/primitives#1166

Turns out that Reflex's StatefulComponent memoization is not handling ref and prop forwarding, which Radix is depending on.

If i change the code to be like this

export const Button_4f9c226eb6db5a68f02ace30e6d02ba7 = forwardRef((props, forwardedRef) => {
  const state = useContext(StateContexts.state)



  return (
    <button ref={forwardedRef} {...props}>
  {`fooey ${state.router.session.client_token}`}
</button>
  )
})

Then <Button_4f9c226eb6db5a68f02ace30e6d02ba7 /> can work as a child of the tooltip.

I wonder if it makes sense to eventually structure all of the memoized components in this way to maximize compatibility.

@greenseeker
Copy link
Author

@masenf what's that workaround doing?

@masenf
Copy link
Collaborator

masenf commented May 14, 2024

what's that workaround doing?

The workaround is making it so that child of the tooltip does not get memoized separately. The IntegralFragment becomes its own React component with the state context of all its children inside.

@abulvenz
Copy link
Contributor

abulvenz commented May 29, 2024

I have another minimal example, which causes the same error and @masenf 's workaround makes it work.

import reflex as rx

class SimpleState(rx.State):
    key: str = "Don't open menu"

@rx.page(route="/page2")
def page2() -> rx.Component:
    return rx.menu.root(
    rx.menu.trigger(
        rx.button(SimpleState.key),
    ),
    rx.menu.content(
        rx.menu.item("Edit", shortcut="⌘ E"),
   ),
)

The menu is rendered correctly, but as the inscription on the button promises, the menu will not open. Even if you change the text to "Open menu".

I can not oversee if this is the exact same problem or a different one. If different, please let me know and I will create a new issue.

@masenf
Copy link
Collaborator

masenf commented May 29, 2024

is the exact same problem

yes, i believe it's the same issue. Radix is wanting to pass a ref down into the component wrapped by the trigger/tooltip/close/etc and the memoized StatefulComponent code just does not support it, so it doesn't inherit the special behavior.

I think the fix is relatively clear, I just haven't had time to put it in and test it.

@abulvenz
Copy link
Contributor

abulvenz commented Jul 1, 2024

@masenf I just encountered it again (see below).
Even with the workaround (hopefully applied correctly (I'm not a react-expert)) it doesn't work (OK, the error is gone, but the click not propagated).
Sorry for the messy example, I tried to isolate it as good as possible.

# index.py in fresh sidebar app
from reflex_custom_components_playground.templates import template  

import reflex as rx
from reflex.components.component import MemoizationLeaf
from reflex.components.base.fragment import Fragment


class IntegralFragment(Fragment, MemoizationLeaf):
    pass


class IndexState(rx.State):
    icon: str = "home"

    def set_skull(self) -> None:
        self.icon = "skull"

    def set_trash(self) -> None:
        self.icon = "trash"


@template(route="/", title="Home")
def index() -> rx.Component:

    return rx.vstack(
        rx.text(IndexState.icon),
        rx.table.root(
            rx.table.header(
                rx.table.row(
                    rx.table.cell(),
                    rx.table.column_header_cell(
                        "Delete without asking",
                        IntegralFragment.create(
                            rx.tooltip(
                                rx.icon(
                                    tag="skull",
                                    on_click=IndexState.set_skull,
                                ),
                                content="Skull icon",
                            ),
                        ),
                    ),
                    rx.table.column_header_cell(
                        "Delete asking politely",
                        rx.dialog.root(
                            rx.dialog.trigger(
                                IntegralFragment.create(
                                    rx.tooltip(
                                        rx.icon(
                                            tag="trash-2",
                                            # on_click=IndexState.set_trash,
                                        ),
                                        content="Trash icon",
                                    ),
                                ),
                            ),
                            rx.dialog.content(
                                rx.dialog.title("action.name"),
                                rx.dialog.description(
                                    "action.confirmation_description"
                                ),
                                rx.dialog.close(
                                    rx.hstack(
                                        rx.button(
                                            "action.name",
                                            size="3",
                                            id="confirm",
                                            background_color="green",
                                            on_click=IndexState.set_trash,  # type: ignore[reportArgumentType]
                                        ),
                                        rx.button(
                                            "Cancel",
                                            size="3",
                                            background_color="red",
                                            id="abort",
                                        ),
                                    )
                                ),
                            ),
                        ),
                    ),
                ),
            ),
            rx.table.body(
                rx.table.row(
                    rx.table.cell("John Doe"),
                    rx.table.cell(
                        "Delete without asking",
                        IntegralFragment.create(
                            rx.tooltip(
                                rx.icon(
                                    tag="skull",
                                    on_click=IndexState.set_skull,
                                ),
                                content="Skull icon",
                            ),
                        ),
                    ),
                    rx.table.cell(
                        "Delete asking politely",
                        rx.dialog.root(
                            rx.dialog.trigger(
                                IntegralFragment.create(
                                    rx.tooltip(
                                        rx.icon(
                                            tag="trash-2",
                                            # on_click=IndexState.set_trash,
                                        ),
                                        content="Trash icon",
                                    ),
                                ),
                            ),
                            rx.dialog.content(
                                rx.dialog.title("action.name"),
                                rx.dialog.description(
                                    "action.confirmation_description"
                                ),
                                rx.dialog.close(
                                    rx.hstack(
                                        rx.button(
                                            "action.name",
                                            size="3",
                                            id="confirm",
                                            background_color="green",
                                            on_click=IndexState.set_trash,  # type: ignore[reportArgumentType]
                                        ),
                                        rx.button(
                                            "Cancel",
                                            size="3",
                                            background_color="red",
                                            id="abort",
                                        ),
                                    )
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        ),
    )

@abulvenz
Copy link
Contributor

Sometimes you need multiple boxes. 🧑‍🔧

    rx.dialog.trigger(
        rx.box(
            rx.tooltip(
                rx.box(button),
                content=action.name,
            )
        )
    ),

It feels like in sokoban. 😄

@abulvenz
Copy link
Contributor

Then <Button_4f9c226eb6db5a68f02ace30e6d02ba7 /> can work as a child of the tooltip.

I wonder if it makes sense to eventually structure all of the memoized components in this way to maximize compatibility.

@masenf Did you reach a conclusion concerning this?
Unfortunately I cannot be of any help here due to a lack of react comprehension.

@abulvenz
Copy link
Contributor

@masenf In their documentation they write it is best practice to always use forwardRef
Whilst this isn't necessary for all parts, we recommend always doing it so that you are not concerned with implementation details. This is also generally good practice anyway for leaf components.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants