JohnCoene / cicerone

🏛️ Give tours of your Shiny apps
https://cicerone.john-coene.com
Other
187 stars 7 forks source link

Highlight "search" area in DataTable #33

Closed etiennebacher closed 2 years ago

etiennebacher commented 2 years ago

This PR tries to solve #28.

Here's the example in the issue:

library(shiny)
library(DT)

guide <- Cicerone$
  new()$ 
  step(
    el = "tbl",
    title = "DT table",
    description = "This is a DT table"
  )$
  step(
    el ="DataTables_Table_0_filter",
    "Search",
    "This is search"
  )

ui <- fluidPage(
  use_cicerone(), # include dependencies
  br(),
  actionButton("guide", "Guide"),
  DTOutput('tbl')
)

server <- function(input, output){

  # initialise the guide
  guide$init()

  observeEvent(input$guide, {
    guide$start()
  })
  output$tbl = renderDT(
    iris, options = list(lengthChange = FALSE)
  )

}

shinyApp(ui, server)

I think the reason why this "Search" area is not detected by driver.js is because driver.js makes the list of the steps before the DataTable is actually rendered, which means that the "search" area is undetected. I wrap the whole driver creation in setTimeout so that the DataTable is rendered first.

This isn't very clean, and the "search" area is not highlighted in some cases (if you click very quickly on the "Guide" button after having launched the app for instance). But it works in many cases, for this example at least.

Just in case it is helpful, I add below the shiny app with the "raw" driver.js code instead of cicerone.

shiny+driver.js example
library(shiny)
library(DT)

ui <- fluidPage(
  shinyjs::useShinyjs(),
  tags$head(
    tags$script(src="https://unpkg.com/driver.js/dist/driver.min.js"),
    tags$link(rel="stylesheet", href="https://unpkg.com/driver.js/dist/driver.min.css") 
  ),
  actionButton("guide", "Guide"),
  DTOutput('tbl'),

)

server <- function(input, output){

  output$tbl = renderDT(
    iris, options = list(lengthChange = FALSE)
  )

  shinyjs::runjs("

  setTimeout(function(){
    const driver = new Driver();

      driver.defineSteps([
       {
         element: '#tbl',
         popover: {
              title: 'Test 1',
              position: 'bottom'
            }
           },
            {
            element: '#DataTables_Table_0_filter',
            popover: {
              title: 'Test 2',
              position: 'bottom'
            }
           }
         ]);
         let btn = document.querySelector('#guide');
         btn.addEventListener('click', function(){
          event.stopPropagation()
          driver.start();
         });
        }, 30)") 
}

shinyApp(ui, server)
JohnCoene commented 2 years ago

Hey Etienne,

It's a good observation that the reason this does not work is because driver.js runs before the table has rendered.

However, I don't think the solution will work in every case. It works in this one because 50 ms is enough but it could not. I'm not sure what the right solution could be but here are some ideas.

Let me know your thoughts.

etiennebacher commented 2 years ago

Hello John,

Actually, in the example of #28, the problem is that the driver is created as soon as the app runs. Putting guide$init() in observeEvent solves the problem for this app.

library(shiny)
library(DT)

guide <- Cicerone$
  new()$ 
  step(
    el = "tbl",
    title = "DT table",
    description = "This is a DT table"
  )$
  step(
    el ="DataTables_Table_0_filter",
    "Search",
    "This is search"
  )

ui <- fluidPage(
  use_cicerone(), # include dependencies
  br(),
  actionButton("guide", "Guide"),
  DTOutput('tbl')
)

server <- function(input, output){

  observeEvent(input$guide, {
    guide$init()$start()
  })
  output$tbl = renderDT({
    iris 
  }, options = list(lengthChange = FALSE))

}

shinyApp(ui, server)

But what if we want the guide to run as soon as the app launches?

It seems to me that there's no good solution that allows to have both a guide that runs when the app starts and a guide that includes an item that takes a long time to render. I think that if an item really takes a long time to render, the guide should be launched via clicking on a button because otherwise, cicerone is just waiting for the item to render and (from the point of view of the user) nothing is happening.

So, providing a timeout option doesn't seem so bad to me, as it is the developer who decides how much delay he/she is willing to give to cicerone before initiating the guide, and this delay is not necessarily noticeable by the user (in the example I used, 50ms is not perceived as a delay, but of course it was a very simple app without much to render). However, I suppose that the time to render elements differ between users according to the internet connection for instance?

In summary:

That's what I think (with my very limited experience in UX and web development), what do you think?

JohnCoene commented 2 years ago

Sorry etienne, j'ai complètement oublié cette PR :(

etiennebacher commented 2 years ago

No worries, I just wanted to clean my fork and it's not like there was much code changed on this PR ;)