Skip to content

Commit

Permalink
Add dropdown menu to header navigation (#754)
Browse files Browse the repository at this point in the history
* Add dropdown menu to header navigation

* Tests

* Timeout fix

* Tests

* Box shadow

* Add try/except for each page

* Flex flow

* Update docs/scripts/generate_gallery_text.py

Co-authored-by: Rambaud Pierrick <12rambau@users.noreply.github.com>

* Delete test_navbar_header_dropdown.html

Co-authored-by: Rambaud Pierrick <12rambau@users.noreply.github.com>
  • Loading branch information
choldgraf and 12rambau committed Jun 28, 2022
1 parent be50b54 commit e1d4009
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 19 deletions.
13 changes: 13 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,22 @@
"url": "https://github.com/pydata/pydata-sphinx-theme/releases",
"name": "Changelog",
},
{
"url": "https://pydata.org",
"name": "PyData",
},
{
"url": "https://numfocus.org/",
"name": "NumFocus",
},
{
"url": "https://numfocus.org/donate",
"name": "Donate to NumFocus",
},
],
"github_url": "https://github.com/pydata/pydata-sphinx-theme",
"twitter_url": "https://twitter.com/PyData",
"header_links_before_dropdown": 4,
"icon_links": [
{
"name": "PyPI",
Expand Down
12 changes: 9 additions & 3 deletions docs/scripts/generate_gallery_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,27 @@ def regenerate_gallery():
gallery_directive_items = []
with sync_playwright() as p:
# Generate our browser to visit pages and generate images
browser = p.chromium.launch()
for ii in range(3):
try:
browser = p.chromium.launch()
break
except TimeoutError:
print(f"Browser start timed out. Trying again (attempt {ii+2}/3)")
page = browser.new_page()

for item in track(gallery_items, description="Generating screenshots..."):
item["id"] = item["name"].lower().replace(" ", "_")
screenshot = gallery_dir / f"{item['id']}.png"

# Visit the page and take a screenshot
for ii in range(3):
try:
page.goto(item["website"])
page.screenshot(path=screenshot)
break
except TimeoutError:
print(f"{item['name']} timed out. Trying again (attempt {ii+2}/3)")
continue
print(f"Page visit start timed out for: {item['website']}")
print(f"Trying again (attempt {ii+2}/3)")

# copy the 404 only if the screenshot file was not manually
# generated by a maintainer
Expand Down
30 changes: 25 additions & 5 deletions docs/user_guide/configuring.rst
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,14 @@ Additionally, the screen-reader accessible label for this menu can be configured
}
Adding external links to your nav bar
=====================================
Header Navigation Bar
=====================

The header navigation bar is at the top of each page and contains top-level navigation across pages in your documentation, as well as extra links and components that you can add.
These sections cover a few things you can control with the Header Navigation Bar.

Add external links
------------------

You can add external links to your navigation bar. These will show up to the right
of your site's main links, and will have a small icon indicating that they point to
Expand All @@ -348,6 +354,23 @@ an external site. You can add external links to the nav bar like so:
]
}
Header dropdown links
---------------------

By default, this theme will display the first **five** navigation links in the header (including both top-level links and external links).
It will place the remaining header links in a **dropdown menu** titled "More".
This prevents the header links from taking up so much space that they crowd out the UI components or spill off screen.

To control how many header links are displayed before being placed in the dropdown, use the ``header_links_before_dropdown`` theme configuration variable.
For example, to change the number of displayed header links to be ``4`` instead of ``5``:abbr:

.. code-block:: python
html_theme_options = {
"header_links_before_dropdown": 4
}
Adding favicons
===============

Expand Down Expand Up @@ -450,9 +473,6 @@ To enable this behavior, set the ``show_nav_level`` value to 0, like below:
"show_nav_level": 0
}
You can only collapse your ``toctree`` items underneath their caption if a caption is defined for them!
If your ``toctree`` does not have a caption defined, then all of the pages underneath it will be displayed
(the same as the default theme behavior). See `the toctree documentation <https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-toctree>`_
Expand Down
52 changes: 48 additions & 4 deletions src/pydata_sphinx_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ def update_templates(app, pagename, templatename, context, doctree):
def add_toctree_functions(app, pagename, templatename, context, doctree):
"""Add functions so Jinja templates can add toctree objects."""

def generate_nav_html(kind, startdepth=None, show_nav_level=1, **kwargs):
def generate_nav_html(
kind, startdepth=None, show_nav_level=1, n_links_before_dropdown=5, **kwargs
):
"""
Return the navigation link structure in HTML. Arguments are passed
to Sphinx "toctree" function (context["toctree"] below).
Expand All @@ -174,6 +176,9 @@ def generate_nav_html(kind, startdepth=None, show_nav_level=1, **kwargs):
By default, this level is 1, and only top-level pages are shown,
with drop-boxes to reveal children. Increasing `show_nav_level`
will show child levels as well.
n_links_before_dropdown : int (default: 5)
The number of links to show before nesting the remaining links in
a Dropdown element.
kwargs: passed to the Sphinx `toctree` template function.
Expand All @@ -191,6 +196,13 @@ def generate_nav_html(kind, startdepth=None, show_nav_level=1, **kwargs):
# select the "active" subset of the navigation tree for the sidebar
toc_sphinx = index_toctree(app, pagename, startdepth, **kwargs)

try:
n_links_before_dropdown = int(n_links_before_dropdown)
except Exception:
raise ValueError(
f"n_links_before_dropdown is not an int: {n_links_before_dropdown}"
)

soup = bs(toc_sphinx, "html.parser")

# pair "current" with "active" since that's what we use w/ bootstrap
Expand All @@ -205,14 +217,46 @@ def generate_nav_html(kind, startdepth=None, show_nav_level=1, **kwargs):
if "#" in href and href != "#":
li.decompose()

# For navbar, generate only top-level links and add external links
if kind == "navbar":
links = soup("li")

# Add CSS for bootstrap
for li in soup("li"):
for li in links:
li["class"].append("nav-item")
li.find("a")["class"].append("nav-link")
# only select li items (not eg captions)
out = "\n".join([ii.prettify() for ii in soup.find_all("li")])

# Convert to HTML so we can append external links
links_html = [ii.prettify() for ii in links]

# Add external links
for external_link in context["theme_external_links"]:
links_html.append(
f"""
<li class="nav-item">
<a class="nav-link nav-external" href="{ external_link["url"] }">{ external_link["name"] }<i class="fas fa-external-link-alt"></i></a>
</li>""" # noqa
)

# Wrap the final few header items in a "more" block
links_solo = links_html[:n_links_before_dropdown]
links_dropdown = links_html[n_links_before_dropdown:]

out = "\n".join(links_solo)
if links_dropdown:
links_dropdown_html = "\n".join(links_dropdown)
out += f"""
<div class="nav-item dropdown">
<button class="btn dropdown-toggle nav-item" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
More
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
{links_dropdown_html}
</div>
</div>
""" # noqa

# For sidebar, we generate links starting at the second level of the active page
elif kind == "sidebar":
# Add bootstrap classes for first `ul` items
for ul in soup("ul", recursive=False):
Expand Down
24 changes: 23 additions & 1 deletion src/pydata_sphinx_theme/assets/styles/sections/_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#navbar-start {
display: flex;
align-items: center;
flex-flow: wrap;
}

#navbar-end,
Expand Down Expand Up @@ -49,9 +50,12 @@

.navbar-nav {
display: flex;
// Add a gap on wider screens, on narrow screens we are vertical + already have padding

@include media-breakpoint-up(lg) {
// Add a gap on wider screens, on narrow screens we are vertical + already have padding
gap: 1rem;
// Center align on wide screens so the dropdown button is centered properly
align-items: center;
}

li a.nav-link {
Expand All @@ -73,6 +77,24 @@
font-weight: 600;
color: var(--pst-color-primary);
}

// Dropdowns for the extra links
.dropdown {
z-index: $zindex-popover;
height: 2.2rem; // Slight hack to make this aligned with navbar links

button {
color: var(--pst-color-text-muted);
}

.dropdown-menu {
color: var(--pst-color-text-base);
background-color: var(--pst-color-on-background);
box-shadow: 0 0 0.3rem 0.1rem var(--pst-color-shadow);
padding: 0.5rem 1rem;
min-width: 20rem;
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
<ul id="navbar-main-elements" class="navbar-nav">
{{ generate_nav_html("navbar", maxdepth=1, collapse=True, includehidden=True, titles_only=True) }}
{% for external_link in theme_external_links %}
<li class="nav-item">
<a class="nav-link nav-external" href="{{ external_link.url }}">{{ _(external_link.name) }}<i class="fas fa-external-link-alt"></i></a>
</li>
{% endfor %}
{{ generate_nav_html("navbar", maxdepth=1, n_links_before_dropdown=theme_header_links_before_dropdown, collapse=True, includehidden=True, titles_only=True) }}
</ul>
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ navbar_align = content
navbar_start = navbar-logo.html
navbar_center = navbar-nav.html
navbar_end = search-button.html, theme-switcher.html, navbar-icon-links.html
header_links_before_dropdown = 5
left_sidebar_end = sidebar-ethical-ads.html
footer_items = copyright.html, sphinx-version.html
page_sidebar_items = page-toc.html, edit-this-page.html, sourcelink.html
Expand Down
31 changes: 31 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,37 @@ def test_navbar_no_in_page_headers(sphinx_build_factory, file_regression):
file_regression.check(navbar.prettify(), extension=".html")


@pytest.mark.parametrize("n_links", (0, 4, 8)) # 0 = only dropdown, 8 = no dropdown
def test_navbar_header_dropdown(sphinx_build_factory, file_regression, n_links):
"""Test whether dropdown appears based on number of header links + config."""
extra_links = [{"url": f"https://{ii}.org", "name": ii} for ii in range(3)]

confoverrides = {
"html_theme_options": {
"external_links": extra_links,
"header_links_before_dropdown": n_links,
}
}
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
index_html = sphinx_build.html_tree("index.html")
navbar = index_html.select("ul#navbar-main-elements")[0]
if n_links == 0:
# There should be *only* a dropdown and no standalone links
assert navbar.select("div.dropdown") and not navbar.select(
".navbar-nav > li.nav-item"
) # noqa
if n_links == 4:
# There should be at least one standalone link, and a dropdown
assert navbar.select(".navbar-nav > li.nav-item") and navbar.select(
"div.dropdown"
) # noqa
if n_links == 8:
# There should be no dropdown and only standalone links
assert navbar.select(".navbar-nav > li.nav-item") and not navbar.select(
"div.dropdown"
) # noqa


def test_sidebars_captions(sphinx_build_factory, file_regression):
sphinx_build = sphinx_build_factory("sidebars").build()

Expand Down

0 comments on commit e1d4009

Please sign in to comment.