rstudio / rstudio/bslib

Scrolling disabled after opening modal from within another modal

Open
#733 11 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
SCSS
Stars
569
Forks
72
Avg merge
1d 2h
Merged PRs (30d)
5

Description

Opening a modal using a button in another modal disables scrolling because it leaves the html body tag with (among other things) style = overflow: hidden.

  • This happens regardless of whether the first modal gets closed with removeModal() before opening the second modal.
  • This happens regardless of whether fade is set to TRUE or FALSE
  • This does not happen with shiny::pageFluid(), only with bslib::page_fluid() and potentially other bslib layout functions (not tested)
  • The pattern of opening a modal from within another modal is recommended in the examples in the Shiny documentation, so this is expected to work (in the example, a call to removeModal() before opening the second modal is also not necessary). See example with confirmation modal here.

The temporary styles on the body tag are set to prevent scrolling when a modal is open, but they do not get removed properly under these circumstances.

The reason seems to be that data-bs-padding-right and data-bs-overflow data attributes are set on the body tag when the modal is opened but they don't get removed. There seems to be some listener that looks for these attributes and, based on their content, sets the temporary styles.

I'm not entirely sure if this is purely bslib's "fault" or if this is a bootstrap issue. In any case, it is easy to miss this bug when implementing a standard Shiny pattern (a confirmation modal, see above) and can severely break an app.

library(shiny)
library(bslib)

ui <- bslib::page_fluid(  # NB: this works with `shiny::fluidPage()`
  div(
    style = "min-height: 150vh;",
    shiny::checkboxInput("inp_fade_modals", "Fade Modals"),
    shiny::checkboxInput("inp_close_modals", "Close Modal 1 before Modal 2"),
    shiny::checkboxInput("inp_allow_easy_close", "Allow easy close"),
    shiny::actionButton("btn_open_modal", "Open Modal 1"),
    tags$ul(id = "log")
  ),
  div("Try to scroll past this.", style = "position: absolute; bottom: 0;"),
  div("Hello there!", style = "position: relative; bottom: 0;"),
  
  # Log the body tag to the interface for convenience
  
  tags$head(
    tags$script(HTML("
      $(document).ready(function() {
        const bodyTag = document.querySelector('body');
        
        logBodyTag = function() {
          let tag = bodyTag.outerHTML.split('>', 1)[0].substring(1)
          let hasError = !bodyTag.classList.contains('modal-open') && bodyTag.style.overflow == 'hidden';
          console.log(tag)
          
          let logEntry = $('#log').append(`<li>&lt;${ tag }&gt;</li>`)
          if (hasError) {
            logEntry.find('li:last').addClass('text-danger');
          }
        }
      
        logBodyTag()  // also log once at start
      
        const config = { attributes: true, childList: false, subtree: false };
        const observer = new MutationObserver(callback = logBodyTag)
        observer.observe(bodyTag, config);
      });
    "))
  )
)


server <- function(input, output) {
  
  # Opening a modal from within another modal disables scrolling on the 
  # body by leaving the html body tag with `style = overflow: hidden`
  
  observeEvent(
    input$btn_open_modal,
    {
      showModal(modalDialog(
        "Modal 1",
        footer    = tagList(
          modalButton("Cancel"),
          actionButton("btn_in_modal1", "Open Modal 2")
        ),
        fade      = input$inp_fade_modals,
        easyClose = input$inp_allow_easy_close
      ))
    }
  )
  
  observeEvent(
    input$btn_in_modal1,
    {
      if (input$inp_close_modals) removeModal()
      
      showModal(modalDialog(
        "Modal 2",
        footer    = tagList(
          modalButton("Cancel"),
          actionButton("btn_in_modal2", "Close Modal 2")
        ),
        fade      = input$inp_fade_modals,
        easyClose = input$inp_allow_easy_close
      ))
    }
  )
  
  observeEvent(
    input$btn_in_modal2,
    {
      # do something here ...
      removeModal()
    }
  )
  
}

shiny::shinyApp(ui, server)
Session Info

─ Session info ────────────────────────────────────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.2.2 (2022-10-31 ucrt)
 os       Windows 10 x64 (build 19045)
 system   x86_64, mingw32
 ui       RStudio
 language (EN)
 collate  German_Germany.utf8
 ctype    German_Germany.utf8
 tz       Europe/Berlin
 date     2023-08-07
 rstudio  2023.06.1+524 Mountain Hydrangea (desktop)
 pandoc   NA

─ Packages ────────────────────────────────────────────────────────────────────────────────────────────────
package * version date (UTC) lib source
bslib * 0.5.0.9000 2023-08-07 [1] Github (rstudio/bslib@1a763b8)
cachem 1.0.6 2021-08-19 [1] CRAN (R 4.2.1)
callr 3.7.0 2021-04-20 [1] CRAN (R 4.2.1)
cli 3.3.0 2022-04-25 [1] CRAN (R 4.2.1)
crayon 1.5.1 2022-03-26 [1] CRAN (R 4.2.1)
curl 4.3.2 2021-06-23 [1] CRAN (R 4.2.1)
devtools 2.4.4 2022-07-20 [1] CRAN (R 4.2.1)
digest 0.6.29 2021-12-01 [1] CRAN (R 4.2.1)
ellipsis 0.3.2 2021-04-29 [1] CRAN (R 4.2.1)
fastmap 1.1.0 2021-01-25 [1] CRAN (R 4.2.1)
fs 1.5.2 2021-12-08 [1] CRAN (R 4.2.1)
glue 1.6.2 2022-02-24 [1] CRAN (R 4.2.1)
htmltools 0.5.5 2023-03-23 [1] CRAN (R 4.2.3)
htmlwidgets 1.5.4 2021-09-08 [1] CRAN (R 4.2.1)
httpuv 1.6.5 2022-01-05 [1] CRAN (R 4.2.1)
jquerylib 0.1.4 2021-04-26 [1] CRAN (R 4.2.1)
jsonlite 1.8.0 2022-02-22 [1] CRAN (R 4.2.1)
later 1.3.0 2021-08-18 [1] CRAN (R 4.2.1)
lifecycle 1.0.1 2021-09-24 [1] CRAN (R 4.2.1)
magrittr 2.0.3 2022-03-30 [1] CRAN (R 4.2.1)
memoise 2.0.1 2021-11-26 [1] CRAN (R 4.2.1)
mime 0.12 2021-09-28 [1] CRAN (R 4.2.0)
miniUI 0.1.1.1 2018-05-18 [1] CRAN (R 4.2.0)
pkgbuild 1.3.1 2021-12-20 [1] CRAN (R 4.2.1)
pkgload 1.3.0 2022-06-27 [1] CRAN (R 4.2.1)
prettyunits 1.1.1 2020-01-24 [1] CRAN (R 4.2.1)
processx 3.6.1 2022-06-17 [1] CRAN (R 4.2.1)
profvis 0.3.7 2020-11-02 [1] CRAN (R 4.2.1)
promises 1.2.0.1 2021-02-11 [1] CRAN (R 4.2.1)
ps 1.7.1 2022-06-18 [1] CRAN (R 4.2.1)
purrr 0.3.4 2020-04-17 [1] CRAN (R 4.2.1)
R6 2.5.1 2021-08-19 [1] CRAN (R 4.2.1)
Rcpp 1.0.9 2022-07-08 [1] CRAN (R 4.2.1)
remotes 2.4.2 2021-11-30 [1] CRAN (R 4.2.1)
rlang 1.1.1 2023-04-28 [1] CRAN (R 4.2.3)
rprojroot 2.0.3 2022-04-02 [1] CRAN (R 4.2.1)
rstudioapi 0.13 2020-11-12 [1] CRAN (R 4.2.1)
sass 0.4.7 2023-07-15 [1] CRAN (R 4.2.3)
sessioninfo 1.2.2 2021-12-06 [1] CRAN (R 4.2.1)
shiny * 1.7.1 2021-10-02 [1] CRAN (R 4.2.1)
stringi 1.7.6 2021-11-29 [1] CRAN (R 4.2.0)
stringr 1.4.0 2019-02-10 [1] CRAN (R 4.2.1)
urlchecker 1.0.1 2021-11-30 [1] CRAN (R 4.2.1)
usethis 2.1.6 2022-05-25 [1] CRAN (R 4.2.1)
withr 2.5.0 2022-03-03 [1] CRAN (R 4.2.1)
xtable 1.8-4 2019-04-21 [1] CRAN (R 4.2.1)

[1] C:/Users/[...]/AppData/Local/R/win-library/4.2
[2] C:/Program Files/R/R-4.2.2/library

Workaround

As a workaround, I use a wrapper for removeModal() that additionally calls a JS Function. Not pretty, but it seems to work:

# R - use this (instead of `removeModal()`) before opening another modal from within a modal
properlyRemoveModal <- function() {
  session <- shiny::getDefaultReactiveDomain()
  session$sendCustomMessage("properly-remove-modal", message = list())
  shiny::removeModal()
}

Load this function as part of your UI:

// JS
$(document).ready(function() {
  Shiny.addCustomMessageHandler("properly-remove-modal", (msg) => {
    ["data-bs-padding-right", "data-bs-overflow"].map((e) => {
      document.querySelector("body").removeAttribute(e)
    });
  });
});

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the issue with the supplied R Shiny app using bslib::page_fluid(), nested showModal() calls, and the removeModal() and fade variants. Start by tracing Bootstrap modal handling around the body data-bs-padding-right and data-bs-overflow attributes. Done means opening and closing nested modals restores scrolling without stale attributes across the reported variants.

Written by the indexing model from the issue text.

Assessment

Tech stack
bootstrap, javascript, r, sass
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.