cicerone icon indicating copy to clipboard operation
cicerone copied to clipboard

Highlight "search" area in DataTable

Open etiennebacher opened this issue 3 years ago • 2 comments

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)

etiennebacher avatar Nov 16 '21 21:11 etiennebacher

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.

  • Allow the user to launch after a certain element has rendered as done with {waiter} here (not ideal)
  • Ideally driver.js (launch) only kicks in only after all initial reactives have rendered, perhaps there is an available event
  • Worst case, let the user customise said timeout

Let me know your thoughts.

JohnCoene avatar Nov 17 '21 18:11 JohnCoene

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:

  • your first two points could work if the elements that cicerone need to wait for don't take a long time to render, but otherwise it can create a long delay and hence a bad user experience.
  • giving the developer a timeout choice doesn't seem so bad to me, as a very small delay can be sufficient for elements needed to render
  • finally, I think that driver.js throws an error in the console if some elements in the steps don't exist when the driver is created (but need to check that). If this is the case, maybe it could be possible to put a message in the R console to tell the developer that there's a problem with that?

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

etiennebacher avatar Nov 19 '21 19:11 etiennebacher

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

JohnCoene avatar Aug 29 '22 19:08 JohnCoene

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

etiennebacher avatar Aug 29 '22 19:08 etiennebacher