Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for tabs in BS4 #1694

Merged
merged 29 commits into from
Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# pkgdown (development version)

* pkgdown, for Bootstrap 4, supports tabsets in articles [as in R Markdown](https://bookdown.org/yihui/rmarkdown-cookbook/html-tabs.html) including [fading effect](https://bookdown.org/yihui/rmarkdown/html-document.html#tabbed-sections) (@JamesHWade, #1667).
maelle marked this conversation as resolved.
Show resolved Hide resolved

* New template option `trailingslash_redirect` that allows adding a script to redirect `your-package-url.com` to `your-package-url.com/`. (#1439, @cderv, @apreshill)

* `build_reference()` now runs examples with two more local options `rlang_interactive = FALSE` (therefore ensuring non-interactive behavior even in interactive sessions -- see `rlang::is_interactive()`) and `cli.dynamic = FALSE`, `withr::local_envvar(RSTUDIO = NA)` and `withr::local_collate("C")`(#1693).
Expand Down
143 changes: 141 additions & 2 deletions R/html-tweak.R
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ tweak_class_prepend <- function(x, class) {
invisible()
}

has_class <- function(html, class) {
classes <- strsplit(xml2::xml_attr(html, "class"), " ")
purrr::map_lgl(classes, ~ class %in% .x)
}

# from https://github.com/rstudio/bookdown/blob/ed31991df3bb826b453f9f50fb43c66508822a2d/R/bs4_book.R#L307
tweak_footnotes <- function(html) {
container <- xml2::xml_find_all(html, ".//div[@class='footnotes']")
Expand All @@ -129,6 +134,133 @@ tweak_footnotes <- function(html) {
xml2::xml_remove(container)
}

# Tabsets tweaking: find Markdown recommended in https://bookdown.org/yihui/rmarkdown-cookbook/html-tabs.html
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cderv is this transformation something that you might consider hosting in an Rmd-adjacent package at some point in the future? Or do you think the xml2 based transformation is too foreign for the Rmd ecosystem?

Copy link
Contributor

@cderv cderv Jun 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are currently discussing a bit tabset feature because of rstudio/rmarkdown#2054. We got input from @cpsievert that bslib will offer functions for creating navs / tabs UI (https://rstudio.github.io/bslib/reference/index.html#section-create-navs-and-navbars) and that we may be able to leverage that in some way in the future for rmarkdown.

Until now our direction for reworking tabset feature would be to use Lua filters rather than R directly. Also because Quarto has already a Lua filter for this (but using a fenced div syntax). But nothing is planned and started on this.

I think Lua filter would not help at all pkgdown usage currently, am I right ?

If we don't go the Lua filter road, then yes regarding xml2, I think it could be included in the Rmd ecosystem. it has no dependency and I feel we should use it more for some html post processing. But it is not already in the package tree for rmarkdown.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If users use bslib for creating navs, then the resulting divs should be protected i.e. not treated by pkgdown. The code in this PR recognizes future tabsets via the XPath query .//div[contains(@class, 'tabset')] which if I follow correctly would not pick up navs created by bslib. 🤔

# and https://bookdown.org/yihui/rmarkdown/html-document.html#tabbed-sections
# i.e. "## Heading {.tabset}" or "## Heading {.tabset .tabset-pills}"
# no matter the heading level -- the headings one level down are the tabs
# and transform to tabsets HTML a la Bootstrap

tweak_tabsets <- function(html) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's time to start breaking this file up into smaller pieces?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no, it was my favorite R script in pkgdown 😢 More seriously: #1725

tabsets <- xml2::xml_find_all(html, ".//div[contains(@class, 'tabset')]")
purrr::walk(tabsets, tweak_tabset)
invisible(html)
}

tweak_tabset <- function(html) {
maelle marked this conversation as resolved.
Show resolved Hide resolved
id <- xml2::xml_attr(html, "id")

# Users can choose pills or tabs
nav_class <- if (has_class(html, "tabset-pills")) {
"nav-pills"
} else {
"nav-tabs"
}
# Users can choose to make content fade
fade <- has_class(html, "tabset-fade")

# Get tabs and remove them from original HTML
tabs <- xml2::xml_find_all(html, "div")
xml2::xml_remove(tabs)

# Add empty ul for nav and div for content
xml2::xml_add_child(
html,
"ul",
class = sprintf("nav %s nav-row", nav_class),
id = id,
role = "tablist"
)
xml2::xml_add_child(html, "div", class="tab-content")

# Fill the ul for nav and div for content
purrr::walk(tabs, tablist_item, html = html, parent_id = id)
purrr::walk(tabs, tablist_content, html = html, parent_id = id, fade = fade)

# activate first tab unless another one is already activated
# (by the attribute {.active} in the source Rmd)
nav_links <- xml2::xml_find_all(html, sprintf("//ul[@id='%s']/li/a", id))

if (!any(has_class(nav_links, "active"))) {
tweak_class_prepend(nav_links[1], "active")
}

content_div <- xml2::xml_find_first(html, sprintf("//div[@id='%s']/div", id))
if (!any(has_class(xml2::xml_children(content_div), "active"))) {
tweak_class_prepend(xml2::xml_child(content_div), "active")
if (fade) {
tweak_class_prepend(xml2::xml_child(content_div), "show")
}
}
}

# Add an item (tab) to the tablist
tablist_item <- function(tab, html, parent_id) {
maelle marked this conversation as resolved.
Show resolved Hide resolved
id <- xml2::xml_attr(tab, "id")
text <- xml_text1(xml2::xml_child(tab))
ul_nav <- xml2::xml_find_first(html, sprintf("//ul[@id='%s']", parent_id))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think this would be needed as HTML here is supposed to be the tabset, but if I didn't use the ID, all tabs ended up in the first tabset when there were two tabsets.


# Activate (if there was "{.active}" in the source Rmd)
active <- has_class(tab, "active")
class <- if (active) {
"nav-link active"
} else {
"nav-link"
}

xml2::xml_add_child(
ul_nav,
"a",
text,
`data-toggle` = "tab",
href = paste0("#", id),
role = "tab",
`aria-controls` = id,
`aria-selected` = tolower(as.character(active)),
class = class
)

# tab a's need to be wrapped in li's
xml2::xml_add_parent(
xml2::xml_find_first(html, sprintf("//a[@href='%s']", paste0("#", id))),
"li",
role = "presentation",
class = "nav-item"
)
}

# Add content of a tab to a tabset
tablist_content <- function(tab, html, parent_id, fade) {
active <- has_class(tab, "active")

# remove first child, that is the header
xml2::xml_remove(xml2::xml_child(tab))

xml2::xml_attr(tab, "class") <- "tab-pane"
if (fade) {
tweak_class_prepend(tab, "fade")
}

# Activate (if there was "{.active}" in the source Rmd)
if (active) {
tweak_class_prepend(tab, "active")
if (fade) {
tweak_class_prepend(tab, "show")
}
}

xml2::xml_attr(tab, "role") <- "tabpanel"
xml2::xml_attr(tab, " aria-labelledby") <- xml2::xml_attr(tab, "id")

content_div <- xml2::xml_find_first(
html,
sprintf("//div[@id='%s']/div", parent_id)
)

xml2::xml_add_child(content_div, tab)
}



# File level tweaks --------------------------------------------

tweak_rmarkdown_html <- function(html, input_path, pkg = pkg) {
Expand All @@ -137,13 +269,20 @@ tweak_rmarkdown_html <- function(html, input_path, pkg = pkg) {
tweak_anchors(html, only_contents = FALSE)
tweak_md_links(html)
tweak_all_links(html, pkg = pkg)
if (pkg$bs_version > 3) tweak_footnotes(html)

if (pkg$bs_version > 3) {
# Tweak footnotes
tweak_footnotes(html)

# Tweak tabsets
tweak_tabsets(html)
}

# Tweak classes of navbar
toc <- xml2::xml_find_all(html, ".//div[@id='tocnav']//ul")
xml2::xml_attr(toc, "class") <- "nav nav-pills nav-stacked"

# Mame sure all images use relative paths
# Make sure all images use relative paths
img <- xml2::xml_find_all(html, "//img")
src <- xml2::xml_attr(img, "src")
abs_src <- is_absolute_path(src)
Expand Down
32 changes: 32 additions & 0 deletions inst/assets/BS4/pkgdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,35 @@ summary {
details p {
margin-top: -.5rem;
}

/* tabsets */
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another tweak that might make sense would be more padding/margin at the top of tab content.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there normally some styling around the content of the tabs? For the pills example in particular, it's hard to tell how the tab names are related to the content.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added left and bottom borders for the pills example. Its color won't be hard-coded forever, since the blslib variables PR will prevent this kind of hard-coding.

.nav-row {
flex-direction: row;
}

.tab-content {
padding: 1rem;
}

.tabset-pills .tab-content {
border-left: solid 1px #e5e5e5;
border-bottom: solid 1px #e5e5e5;
}

/* https://observablehq.com/@rkaravia/css-trick-tabs-with-consistent-height */
maelle marked this conversation as resolved.
Show resolved Hide resolved
/* Make tab height consistent */

.tab-content {
display: flex;
}

.tab-content > .tab-pane {
display: block; /* undo "display: none;" */
visibility: hidden;
margin-right: -100%;
width: 100%;
}

.tab-content > .active {
visibility: visible;
}
78 changes: 78 additions & 0 deletions tests/testthat/_snaps/html-tweak.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,81 @@
[1] <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role= ...
[2] <div class="dropdown-menu" aria-labelledby="navbarDropdown">\n <a clas ...

# tweak_tabsets() default

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div id="results-in-tabset" class="section level2 tabset">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>


<ul class="nav nav-tabs nav-row" id="results-in-tabset" role="tablist">
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-1" role="tab" aria-controls="tab-1" aria-selected="false" class="active nav-link">Tab 1</a></li>
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-2" role="tab" aria-controls="tab-2" aria-selected="false" class="nav-link">Tab 2</a></li>
</ul>
<div class="tab-content">
<div id="tab-1" class="active tab-pane" role="tabpanel" aria-labelledby="tab-1">

<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="tab-pane" role="tabpanel" aria-labelledby="tab-2">

<p>blop</p>
</div>
</div>
</div></body></html>

# tweak_tabsets() with tab pills and second tab active

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div id="results-in-tabset" class="section level2 tabset tabset-pills">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>


<ul class="nav nav-pills nav-row" id="results-in-tabset" role="tablist">
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-1" role="tab" aria-controls="tab-1" aria-selected="false" class="nav-link">Tab 1</a></li>
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-2" role="tab" aria-controls="tab-2" aria-selected="true" class="nav-link active">Tab 2</a></li>
</ul>
<div class="tab-content">
<div id="tab-1" class="tab-pane" role="tabpanel" aria-labelledby="tab-1">

<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="active tab-pane" role="tabpanel" aria-labelledby="tab-2">

<p>blop</p>
</div>
</div>
</div></body></html>

# tweak_tabsets() with tab pills, fade and second tab active

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div id="results-in-tabset" class="section level2 tabset tabset-pills tabset-fade">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>


<ul class="nav nav-pills nav-row" id="results-in-tabset" role="tablist">
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-1" role="tab" aria-controls="tab-1" aria-selected="false" class="nav-link">Tab 1</a></li>
<li role="presentation" class="nav-item"><a data-toggle="tab" href="#tab-2" role="tab" aria-controls="tab-2" aria-selected="true" class="nav-link active">Tab 2</a></li>
</ul>
<div class="tab-content">
<div id="tab-1" class="fade tab-pane" role="tabpanel" aria-labelledby="tab-1">

<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="show active fade tab-pane" role="tabpanel" aria-labelledby="tab-2">

<p>blop</p>
</div>
</div>
</div></body></html>

66 changes: 66 additions & 0 deletions tests/testthat/test-html-tweak.R
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,69 @@ test_that("activate_navbar()", {
xml2::xml_find_first(navbar, ".//li[contains(@class, 'active')]")
)
})

# tabsets -------------------------------------------------------------

test_that("tweak_tabsets() default", {
html <- '<div id="results-in-tabset" class="section level2 tabset">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>
<div id="tab-1" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-1" class="anchor" aria-hidden="true"></a>Tab 1</h3>
<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it important to have source code in here? Otherwise it would be better to keep the test as short and simple as possible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find of another solution right now #1725 (comment)

<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-2" class="anchor" aria-hidden="true"></a>Tab 2</h3>
<p>blop</p>
</div>
</div>'
new_html <- tweak_tabsets(xml2::read_html(html))
expect_snapshot_output(cat(as.character(new_html)))
})

test_that("tweak_tabsets() with tab pills and second tab active", {
html <- '<div id="results-in-tabset" class="section level2 tabset tabset-pills">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>
<div id="tab-1" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-1" class="anchor" aria-hidden="true"></a>Tab 1</h3>
<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="section level3 active">
<h3 class="hasAnchor">
<a href="#tab-2" class="anchor" aria-hidden="true"></a>Tab 2</h3>
<p>blop</p>
</div>
</div>'
new_html <- tweak_tabsets(xml2::read_html(html))
expect_snapshot_output(cat(as.character(new_html)))
})


test_that("tweak_tabsets() with tab pills, fade and second tab active", {
html <- '<div id="results-in-tabset" class="section level2 tabset tabset-pills tabset-fade">
<h2 class="hasAnchor">
<a href="#results-in-tabset" class="anchor" aria-hidden="true"></a>Results in tabset</h2>
<div id="tab-1" class="section level3">
<h3 class="hasAnchor">
<a href="#tab-1" class="anchor" aria-hidden="true"></a>Tab 1</h3>
<p>blablablabla</p>
<div class="sourceCode" id="cb9"><pre class="downlit sourceCode r">
<code class="sourceCode R"><span class="fl">1</span> <span class="op">+</span> <span class="fl">1</span></code></pre></div>
</div>
<div id="tab-2" class="section level3 active">
<h3 class="hasAnchor">
<a href="#tab-2" class="anchor" aria-hidden="true"></a>Tab 2</h3>
<p>blop</p>
</div>
</div>'
new_html <- tweak_tabsets(xml2::read_html(html))
expect_snapshot_output(cat(as.character(new_html)))
})
Loading