Scrolling disabled after opening modal from within another modal
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
fadeis set toTRUEorFALSE - This does not happen with
shiny::pageFluid(), only withbslib::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><${ tag }></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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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