atomiks / tippyjs

Tooltip, popover, dropdown, and menu library
https://atomiks.github.io/tippyjs/
MIT License
12.03k stars 520 forks source link

Tippy with dynamic content that stays present in the DOM #868

Closed sn3p closed 3 years ago

sn3p commented 4 years ago

I'm trying to create a Tippy instance with dynamically populated content generated by a Phoenix LiveView. The LiveView is basically a container which is dynamically updated by the backend (via WebSockets).

This means:

  1. The content needs to be present in the DOM at all times, both when Tippy is shown or hidden.
  2. The content has bindings and listeners so cloning or injecting HTML will not work afaik.

Is there a way to achieve this using Tippy?

Sample of my code:

Template

<button class="target">notifications</button>

<div class="dropdown" style="display: none;">
  <%= live_render(@conn, NotificationsLive, session: %{"user_id" => user_id}) %>
</div>

JavaScript

const target = document.querySelector(".target");
const dropdown = document.querySelector(".dropdown");

dropdown.style.display = "block";

tippy(target, {
  content: dropdown,
  interactive: true
});
sn3p commented 4 years ago

The only workaround I can think of is forcing the tooltip to always remain "open":

tippy(target, {
  // ...
  trigger: "manual",
  showOnCreate: true,
  hideOnClick: false
});

And doing the toggling/transitions manually. But this would also mean I lose the build in features like close on click outside.

skinnaj commented 3 years ago

Hey @sn3p, did you find a way to keep the tooltip in the DOM?

sn3p commented 3 years ago

@Sandburg nope unfortunately not, still looking for it. If you find it please let me know :)

sn3p commented 3 years ago

@atomiks do you have any ideas on how this can be achieved?

KubaJastrz commented 3 years ago

I believe this line from the default template is responsible for removing content from the DOM: https://github.com/atomiks/tippyjs/blob/38caf47592f37cfec6fbb8c2f6220df8ccc1025c/src/template.ts#L40

Maybe if you could use a custom render prop then it would work? Here are docs for headless tippy.

atomiks commented 3 years ago

I think the simplest option here is to move the content to document.body (or some other container) in onHidden, then move it back into the tippy onShow.

const target = document.querySelector(".target");
const dropdown = document.querySelector(".dropdown");

dropdown.style.display = "block";

tippy(target, {
  content: dropdown,
  interactive: true,
  onHidden() {
    document.body.append(dropdown);
  },
  onShow({popper}) {
    popper.firstElementChild.append(dropdown);
  }
});

onHidden gets called after the tippy has been removed from the DOM but this should happen sync, it might cause an issue though.

atomiks commented 3 years ago

I've enabled Discussions for all questions to go inside instead of Issues.

sn3p commented 3 years ago

Thanks @atomiks your suggestion put me on the right track!

dwarfmondo commented 2 years ago

@sn3p I'm dealing with a similar issue. Would you mind sharing how you solved this? I started a discussion here. Thanks!

sn3p commented 2 years ago

Hey @dwarfmondo, this is what I did (working demo). Hope it helps!

const target = document.querySelector(".target");
const dropdown = document.querySelector(".dropdown");
const content = document.querySelector(".content");

tippy(target, {
  content: dropdown,
  onShow: () => {
    // Move content to the dropdown on show.
    dropdown.append(content);
  },
  onHidden: () => {
    // Move content outside the dropdown on hide.
    // This way it remains present in the DOM so it can receive updates.
    document.body.append(content);
  },
});
<button class="target">Hover me</button>
<div class="dropdown"></div>
<div class="content">Dynamic content</div>
.content {
  display: none;
}

.dropdown .content {
  display: block;
}
dwarfmondo commented 2 years ago

@sn3p Perfect. Thank you!!!

msykes commented 2 years ago

@sn3p Thanks for following up with the solution, great workaround, works perfectly for my scenario.