Skip to content

Commit e1d4009

Browse files
choldgraf12rambau
andauthored
Add dropdown menu to header navigation (#754)
* 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 <[email protected]> * Delete test_navbar_header_dropdown.html Co-authored-by: Rambaud Pierrick <[email protected]>
1 parent be50b54 commit e1d4009

File tree

8 files changed

+151
-19
lines changed

8 files changed

+151
-19
lines changed

docs/conf.py

+13
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,22 @@
8787
"url": "https://github.com/pydata/pydata-sphinx-theme/releases",
8888
"name": "Changelog",
8989
},
90+
{
91+
"url": "https://pydata.org",
92+
"name": "PyData",
93+
},
94+
{
95+
"url": "https://numfocus.org/",
96+
"name": "NumFocus",
97+
},
98+
{
99+
"url": "https://numfocus.org/donate",
100+
"name": "Donate to NumFocus",
101+
},
90102
],
91103
"github_url": "https://github.com/pydata/pydata-sphinx-theme",
92104
"twitter_url": "https://twitter.com/PyData",
105+
"header_links_before_dropdown": 4,
93106
"icon_links": [
94107
{
95108
"name": "PyPI",

docs/scripts/generate_gallery_text.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -55,21 +55,27 @@ def regenerate_gallery():
5555
gallery_directive_items = []
5656
with sync_playwright() as p:
5757
# Generate our browser to visit pages and generate images
58-
browser = p.chromium.launch()
58+
for ii in range(3):
59+
try:
60+
browser = p.chromium.launch()
61+
break
62+
except TimeoutError:
63+
print(f"Browser start timed out. Trying again (attempt {ii+2}/3)")
5964
page = browser.new_page()
6065

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

70+
# Visit the page and take a screenshot
6571
for ii in range(3):
6672
try:
6773
page.goto(item["website"])
6874
page.screenshot(path=screenshot)
6975
break
7076
except TimeoutError:
71-
print(f"{item['name']} timed out. Trying again (attempt {ii+2}/3)")
72-
continue
77+
print(f"Page visit start timed out for: {item['website']}")
78+
print(f"Trying again (attempt {ii+2}/3)")
7379

7480
# copy the 404 only if the screenshot file was not manually
7581
# generated by a maintainer

docs/user_guide/configuring.rst

+25-5
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,14 @@ Additionally, the screen-reader accessible label for this menu can be configured
332332
}
333333
334334
335-
Adding external links to your nav bar
336-
=====================================
335+
Header Navigation Bar
336+
=====================
337+
338+
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.
339+
These sections cover a few things you can control with the Header Navigation Bar.
340+
341+
Add external links
342+
------------------
337343

338344
You can add external links to your navigation bar. These will show up to the right
339345
of your site's main links, and will have a small icon indicating that they point to
@@ -348,6 +354,23 @@ an external site. You can add external links to the nav bar like so:
348354
]
349355
}
350356
357+
358+
Header dropdown links
359+
---------------------
360+
361+
By default, this theme will display the first **five** navigation links in the header (including both top-level links and external links).
362+
It will place the remaining header links in a **dropdown menu** titled "More".
363+
This prevents the header links from taking up so much space that they crowd out the UI components or spill off screen.
364+
365+
To control how many header links are displayed before being placed in the dropdown, use the ``header_links_before_dropdown`` theme configuration variable.
366+
For example, to change the number of displayed header links to be ``4`` instead of ``5``:abbr:
367+
368+
.. code-block:: python
369+
370+
html_theme_options = {
371+
"header_links_before_dropdown": 4
372+
}
373+
351374
Adding favicons
352375
===============
353376

@@ -450,9 +473,6 @@ To enable this behavior, set the ``show_nav_level`` value to 0, like below:
450473
"show_nav_level": 0
451474
}
452475
453-
454-
455-
456476
You can only collapse your ``toctree`` items underneath their caption if a caption is defined for them!
457477
If your ``toctree`` does not have a caption defined, then all of the pages underneath it will be displayed
458478
(the same as the default theme behavior). See `the toctree documentation <https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-toctree>`_

src/pydata_sphinx_theme/__init__.py

+48-4
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ def update_templates(app, pagename, templatename, context, doctree):
152152
def add_toctree_functions(app, pagename, templatename, context, doctree):
153153
"""Add functions so Jinja templates can add toctree objects."""
154154

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

199+
try:
200+
n_links_before_dropdown = int(n_links_before_dropdown)
201+
except Exception:
202+
raise ValueError(
203+
f"n_links_before_dropdown is not an int: {n_links_before_dropdown}"
204+
)
205+
194206
soup = bs(toc_sphinx, "html.parser")
195207

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

220+
# For navbar, generate only top-level links and add external links
208221
if kind == "navbar":
222+
links = soup("li")
223+
209224
# Add CSS for bootstrap
210-
for li in soup("li"):
225+
for li in links:
211226
li["class"].append("nav-item")
212227
li.find("a")["class"].append("nav-link")
213-
# only select li items (not eg captions)
214-
out = "\n".join([ii.prettify() for ii in soup.find_all("li")])
215228

229+
# Convert to HTML so we can append external links
230+
links_html = [ii.prettify() for ii in links]
231+
232+
# Add external links
233+
for external_link in context["theme_external_links"]:
234+
links_html.append(
235+
f"""
236+
<li class="nav-item">
237+
<a class="nav-link nav-external" href="{ external_link["url"] }">{ external_link["name"] }<i class="fas fa-external-link-alt"></i></a>
238+
</li>""" # noqa
239+
)
240+
241+
# Wrap the final few header items in a "more" block
242+
links_solo = links_html[:n_links_before_dropdown]
243+
links_dropdown = links_html[n_links_before_dropdown:]
244+
245+
out = "\n".join(links_solo)
246+
if links_dropdown:
247+
links_dropdown_html = "\n".join(links_dropdown)
248+
out += f"""
249+
<div class="nav-item dropdown">
250+
<button class="btn dropdown-toggle nav-item" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
251+
More
252+
</button>
253+
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
254+
{links_dropdown_html}
255+
</div>
256+
</div>
257+
""" # noqa
258+
259+
# For sidebar, we generate links starting at the second level of the active page
216260
elif kind == "sidebar":
217261
# Add bootstrap classes for first `ul` items
218262
for ul in soup("ul", recursive=False):

src/pydata_sphinx_theme/assets/styles/sections/_header.scss

+23-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#navbar-start {
1515
display: flex;
1616
align-items: center;
17+
flex-flow: wrap;
1718
}
1819

1920
#navbar-end,
@@ -49,9 +50,12 @@
4950

5051
.navbar-nav {
5152
display: flex;
52-
// Add a gap on wider screens, on narrow screens we are vertical + already have padding
53+
5354
@include media-breakpoint-up(lg) {
55+
// Add a gap on wider screens, on narrow screens we are vertical + already have padding
5456
gap: 1rem;
57+
// Center align on wide screens so the dropdown button is centered properly
58+
align-items: center;
5559
}
5660

5761
li a.nav-link {
@@ -73,6 +77,24 @@
7377
font-weight: 600;
7478
color: var(--pst-color-primary);
7579
}
80+
81+
// Dropdowns for the extra links
82+
.dropdown {
83+
z-index: $zindex-popover;
84+
height: 2.2rem; // Slight hack to make this aligned with navbar links
85+
86+
button {
87+
color: var(--pst-color-text-muted);
88+
}
89+
90+
.dropdown-menu {
91+
color: var(--pst-color-text-base);
92+
background-color: var(--pst-color-on-background);
93+
box-shadow: 0 0 0.3rem 0.1rem var(--pst-color-shadow);
94+
padding: 0.5rem 1rem;
95+
min-width: 20rem;
96+
}
97+
}
7698
}
7799
}
78100

Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
11
<ul id="navbar-main-elements" class="navbar-nav">
2-
{{ generate_nav_html("navbar", maxdepth=1, collapse=True, includehidden=True, titles_only=True) }}
3-
{% for external_link in theme_external_links %}
4-
<li class="nav-item">
5-
<a class="nav-link nav-external" href="{{ external_link.url }}">{{ _(external_link.name) }}<i class="fas fa-external-link-alt"></i></a>
6-
</li>
7-
{% endfor %}
2+
{{ generate_nav_html("navbar", maxdepth=1, n_links_before_dropdown=theme_header_links_before_dropdown, collapse=True, includehidden=True, titles_only=True) }}
83
</ul>

src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ navbar_align = content
3131
navbar_start = navbar-logo.html
3232
navbar_center = navbar-nav.html
3333
navbar_end = search-button.html, theme-switcher.html, navbar-icon-links.html
34+
header_links_before_dropdown = 5
3435
left_sidebar_end = sidebar-ethical-ads.html
3536
footer_items = copyright.html, sphinx-version.html
3637
page_sidebar_items = page-toc.html, edit-this-page.html, sourcelink.html

tests/test_build.py

+31
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,37 @@ def test_navbar_no_in_page_headers(sphinx_build_factory, file_regression):
262262
file_regression.check(navbar.prettify(), extension=".html")
263263

264264

265+
@pytest.mark.parametrize("n_links", (0, 4, 8)) # 0 = only dropdown, 8 = no dropdown
266+
def test_navbar_header_dropdown(sphinx_build_factory, file_regression, n_links):
267+
"""Test whether dropdown appears based on number of header links + config."""
268+
extra_links = [{"url": f"https://{ii}.org", "name": ii} for ii in range(3)]
269+
270+
confoverrides = {
271+
"html_theme_options": {
272+
"external_links": extra_links,
273+
"header_links_before_dropdown": n_links,
274+
}
275+
}
276+
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
277+
index_html = sphinx_build.html_tree("index.html")
278+
navbar = index_html.select("ul#navbar-main-elements")[0]
279+
if n_links == 0:
280+
# There should be *only* a dropdown and no standalone links
281+
assert navbar.select("div.dropdown") and not navbar.select(
282+
".navbar-nav > li.nav-item"
283+
) # noqa
284+
if n_links == 4:
285+
# There should be at least one standalone link, and a dropdown
286+
assert navbar.select(".navbar-nav > li.nav-item") and navbar.select(
287+
"div.dropdown"
288+
) # noqa
289+
if n_links == 8:
290+
# There should be no dropdown and only standalone links
291+
assert navbar.select(".navbar-nav > li.nav-item") and not navbar.select(
292+
"div.dropdown"
293+
) # noqa
294+
295+
265296
def test_sidebars_captions(sphinx_build_factory, file_regression):
266297
sphinx_build = sphinx_build_factory("sidebars").build()
267298

0 commit comments

Comments
 (0)