-1

Context

I'm trying to have a tooltip popup show on click at the info icon, and hide on click at the icon itself or at any other place of the app. Similarly to how the tooltips in the left panel of SO behave.

Tried so far:

  • The answer provided by thothal below almost have it since it has the desired behavior on click of icons and outside of them. However, it produces a new unwanted behavior that I would need to get rid of. The unwanted behavior is shown and explained in the EDIT section at the end of the question.
  • Borrowed from this SO accepted answer to get the tooltip to show and hide on click of the info icon with data-trigger="click". However, I haven't been able to have it hidden also on click at any other place of the app.
  • Based on this other SO accepted answer, I was able to make the tooltip show on click of the info icon, and hide on click at any other place apart from the icon. However, I haven't been able to make it hide on second click at the icon itself. The code provided in the cited answer does exactly what needed in that MRE, but not in the actual app. The MRE posted here includes the aspects missing in that previous MRE.

Background

This question is related to this other (also posted by me a few days ago) which has already been successfully answered. However, when trying to implement the solution in the actual app I'm working on, it didn't work fully as expected. Further discussion in comments and chat with the person who gave the answer there, let me realize that the following specific aspects of my app, which where not present in my previous MRE, were relevant in order to have the good answer.

New aspects included in this question:

  • The tooltips are created server-side
  • The help texts come from a reactive object
  • Some help texts include html elements

I'm explicitly clarifying this here, hoping to prevent the answer from being marked as duplicate and closed.


An MRE of my code so far

R Shiny file: myApp.R

library(shiny)

# ==============================================================================
js <- "
    $(function () {
      // initialize tooltips
      $('[data-toggle=tooltip]').tooltip({
        html: true
      });
      
      // create listener on html (`everywhere`) which will hide (all) tooltips on click
      $('html').on('click', function(el) {
         $('[data-toggle=tooltip]').tooltip('hide');
      });
      
      // create listener on tooltip elements to toggle the tooltip
      $('[data-toggle=tooltip]').on('click', function(evt) {
         // make sure the click is not bubbling up to <html> to avoid closing it right away
         evt.stopPropagation();
         // hide all other tooltips to ensure there is at most one tooltip open at a time
         $('[data-toggle=tooltip]').not($(this)).tooltip('hide');
         $(this).tooltip('show'); 
      });
    })
    "
# ==============================================================================


# ==============================================================================
ui <- function() {
  fluidPage(
    br(),br(),
    
    fluidRow(
      # Step 1
      uiOutput('head_step1'),
      # here goes some more ui for step 1
      br(),br(),
      
      # Step 2
      uiOutput('head_step2'),
      # here goes some more ui for step 2
      br(),br(),
      
      # Step 3
      uiOutput('head_step3')
      # here goes some more ui for step 3
    )
  )
}
# ==============================================================================


# ==============================================================================
# this function just returns the label matching the specified id
getLabel <- function(dbData, id) {
  dbData$labelsEnlish[dbData$ids == id]
}
# ==============================================================================


# ==============================================================================
server <- function(input, server, output, session) {
  # reactive object containing labels for different languages
  #   - in the actual app this object is much more complex
  #     with multiple languages, many other features inside,
  #     and dependencies on SQL-pulled data.
  #   - here kept reduced to the relevant things for the MRE.
  dbData <- reactiveValues(
    labelsEnlish = c("Step 1: Drop a Pin",
                     "Step 2: Find Public Data",
                     "Step 3: Load Public Data",
                     # help texts for step 1
                     "Click the pin icon",
                     "and then click on a location on the map.",
                     # help texts for step 2
                     "Define the distance and period to query.",
                     "A query for public sites will be available",
                     # help text for step 3
                     "Select the data type(s) to retrieve."
    ),
    ids = c(1, 2, 3, # the three headers
            4, 5, # the help texts for step 1
            6, 7, # the help texts for step 2
            8 # the help text for step 3
    )
  )
  
  # Step 1 - Header
  output$head_step1 <- renderUI({
    span(tags$script(HTML(js)),
         style = "display:inline-block;",
         h4(getLabel(dbData, 1), # 'Step 1: Drop a Pin'
            style = "vertical-align: middle; display: inline;"),
         span(
           `data-toggle` = "tooltip",
           `data-placement` = "bottom",
           `data-trigger` = "manual",
           title = HTML('<p>',
                        getLabel(dbData, 4), # 'Click the pin icon'
                        as.character(icon("map-marker-alt")),
                        getLabel(dbData, 5), # 'and then click on a location on the map.'
                        '</p>'),
           icon("info-circle")
         )
    )
  })
  
  # Step 2 - Header
  output$head_step2 <- renderUI({
    span(tags$script(HTML(js)),
         style = "display:inline-block;",
         h4(getLabel(dbData, 2), # 'Step 2: Find Public Data'
            style = "vertical-align: middle; display: inline;"),
         span(
           `data-toggle` = "tooltip",
           `data-placement` = "bottom",
           `data-trigger` = "manual",
           title = HTML(
             '<div><p>',
             getLabel(dbData, 6), # 'Define the distance and period to query.'
             '</p><p>',
             getLabel(dbData, 7), # 'A query for public sites will be available'
             '</p></div>'),
           icon("info-circle")
         )
    )
  })
  
  # Step 3 - Header
  output$head_step3 <- renderUI({
    span(tags$script(HTML(js)),
         style = "display:inline-block;",
         h4(getLabel(dbData, 3), # 'Step 3: Load Public Data'
            style = "vertical-align: middle; display: inline;"),
         span(
           `data-toggle` = "tooltip",
           `data-placement` = "bottom",
           `data-trigger` = "manual",
           title = getLabel(dbData, 8), # 'Select the data type(s) to retrieve.'
           icon("info-circle")
         )
    )
  })
}
# ==============================================================================


# ==============================================================================
shinyApp(ui = ui, server = server,
         options = list(display.mode = "normal"),
         enableBookmarking = "server")
# ==============================================================================

Note: the person in that same answer told me to invoke the JS code only once, not at each tooltip span, however, I only have an idea of how to do that in the UI, not in the Server. Thus, I'm posting here the code with multiple JS calls, hoping the answer could also help me in that regard.


Desired behavior

  • Shows on first click on icon
  • Hides on second click on icon or on click at any other place

desired behavior


Current behavior

  • Shows on first click on icon
  • Hides on click at any other place
  • Doesn't hide on second click on icon

current behavior


EDIT

  • On March 02, thothal provided a very useful answer to my question. However, it still produces an undesired behavior, which has retained me from marking the answer as correct. In his solution, the tooltips show on click at the info icon, and hide on click at the icon itself or at any other place of the app, BUT, before one of the icons is pressed for the first time, it has an onHover reaction which shows the tooltip as HTML code Here's a clip showing the unwanted onHover behavior:

unwanted in solution

  • This behavior is not visible in the clip attached to the answer, probably because the clicks on the icons were done very quickly or because the record was made after each icon was pressed a first time. I'm sure that the code provided in the answer gives the unwanted behavior shown above.

  • In this edit I also simplified my MRE based on the suggestions made by thothal to make the example actually minimal. I removed the CSS and replaced the marker icon by one that doesn't need to be downloaded. Hope this helps make the question clearer for anyone interested on it.

djbetancourt
  • 347
  • 3
  • 11
  • Does this answer your question? [R Shiny - Hide tooltip when clicked outside](https://stackoverflow.com/questions/75539009/r-shiny-hide-tooltip-when-clicked-outside) – thothal Mar 02 '23 at 08:35
  • When I tried to implement that solution in my app, with my labels comming from a reactiveValues()-produced object and no buttons, the result was the tooltips showing and hidding right away. I think it was my fault and I didn't know how to adapt the answer to my structure. Here I posted the new question with the structure as I have it in my app. I explained in the background section of my question how this is different from the cited one. If the new question gets closed, I'll be left with the solution in a structure which is different from my app and without knowing how to properly adapt it. – djbetancourt Mar 02 '23 at 13:57

1 Answers1

0

Did you try out what I recommended in the cited answer? Apparently not, because if I re-implement the solution it all works as expected.

N.B. Some Changes:

  1. I removed the CSS part as it has nothing to do with the problem at hand and is not really minimal.
  2. I replaced the static picture by icon("map-marker-alt") which gives the same picture but avoids downloading the picture.
  3. I added data-html attribute to the <span> to allow for HTML parsing in the tooltip.
  4. As mentioned in the other post you must not include your JS more than once, otherwise you are calling the toggle event handler a couple of times.
library(shiny)

js <- HTML("
$(function () {
  // create listener on html (`everywhere`) which will hide (all) tooltips on click
  $('html').on('click', function(el) {
     $('[data-toggle=tooltip]').tooltip('hide');
  });
  
  // create another listener on html which, however, fires only
  // for tooltips. This delegation is needed as the tooltips
  // are created dynamically, hence they may not be present at creation time
  $('html').on('click', '[data-toggle=tooltip]', function(evt) {
     // make sure the click is not bubbling up to <html> to avoid closing it right away
     evt.stopPropagation();
     // hide all other tooltips to ensure there is at most one tooltip open at a time
     $('[data-toggle=tooltip]').not($(this)).tooltip('hide');
     $(this).tooltip('toggle');
  });
  
})")

getLabel <- function(dbData, id) {
   dbData$labelsEnlish[dbData$ids == id]
}

ui <- function() {
   fluidPage(
      # include the script _exactly once_
      singleton(tags$head(tags$script(js, type = "application/javascript"))),
      br(),br(),
      fluidRow(
         uiOutput('head_step1'),
         br(),br(),
         uiOutput('head_step2'),
         br(),br(),
         uiOutput('head_step3')
      )
   )
}

server <- function(input, server, output, session) {
   dbData <- reactiveValues(
      labelsEnlish = c("Step 1: Drop a Pin",
                       "Step 2: Find Public Data",
                       "Step 3: Load Public Data",
                       "Click the pin icon",
                       "and then click on a location on the map.",
                       "Define the distance and period to query.",
                       "A query for public sites will be available",
                       "Select the data type(s) to retrieve."
      ),
      ids = c(1, 2, 3, 
              4, 5, 
              6, 7, 
              8
      )
   )
   
   output$head_step1 <- renderUI({
      span(## DO NOT RELOAD THE JS
           style = "display:inline-block;",
           h4(getLabel(dbData, 1), # 'Step 1: Drop a Pin'
              style = "vertical-align: middle; display: inline;"),
           span(
              `data-toggle` = "tooltip",
              `data-placement` = "bottom",
              `data-trigger` = "manual",
              `data-html` = "true",
              title = HTML('<p>',
                           getLabel(dbData, 4), 
                           as.character(icon("map-marker-alt")),
                           getLabel(dbData, 5),
                           '</p>'),
              icon("info-circle")
           )
      )
   })
   

   output$head_step2 <- renderUI({
      span(## DO NOT RELOAD THE JS
         style = "display:inline-block;",
         h4(getLabel(dbData, 2), 
            style = "vertical-align: middle; display: inline;"),
         span(`data-toggle` = "tooltip",
              `data-placement` = "bottom",
              `data-trigger` = "manual",
              `data-html` = "true",
              title = HTML(
                 '<div><p>',
                 getLabel(dbData, 6),
                 '</p><p>',
                 getLabel(dbData, 7), 
                 '</p></div>'),
              icon("info-circle")
         )
      )
   })
   
   output$head_step3 <- renderUI({
      span(## DO NOT RELOAD THE JS
         style = "display:inline-block;",
         h4(getLabel(dbData, 3), 
            style = "vertical-align: middle; display: inline;"),
         span(
              `data-toggle` = "tooltip",
              `data-placement` = "bottom",
              `data-trigger` = "manual",
              `data-html` = "true",
              title = getLabel(dbData, 8), 
              icon("info-circle")
         )
      )
   })
}

Screen recording showing that a click on the icon invokes the tooltip while any other click closes it. This works for consecutive clicks as expected.

thothal
  • 16,690
  • 3
  • 36
  • 71
  • Sure thothal, I tried your recommended solution in the previous question, but I guess I did wrong adapting it to the structure of my app without buttons and ended up breaking the functioning. The result I got was the tooltips showing and hidding right away. Since my code above was closer to the desired behavior, I decided to use that version in the new question. – djbetancourt Mar 02 '23 at 13:30
  • Thanks for this new code. I just tried it exactly as you uploaded it, without any change, and it has the desired behavior on click on the icon and outside. There is still one small undesired behavior, tho. The tooltips are also triggered on hover, which I would like to prevent. And in turn, the tooltip shown on hover displays html code, which is worst (screenshot here: https://drive.google.com/file/d/1U76F4MUdU-vd9DdbFBGv2i_x1Y7-pDn_/view?usp=sharing). Seems the default on hover behavior is still present. Do you think there is some way to remove it? – djbetancourt Mar 02 '23 at 13:44
  • Adding to that, I just noticed the tooltip only shows on hover before one clicks on the icon for the first time. Afterwards, the on hover trigger is not active. – djbetancourt Mar 02 '23 at 14:05
  • Please thothal, we're quite close. You have helped a lot with your answers. The hover trigger I mention above is the only aspect left to address. – djbetancourt Mar 03 '23 at 19:49