Skip to content

Commit 690a4c2

Browse files
alasdairwilsonCadairnabobalis
authored
Add ability to do nested dropdowns in navbar (#291)
Co-authored-by: Stuart Mumford <stuart@cadair.com> Co-authored-by: Nabil Freij <nabil.freij@gmail.com>
1 parent 84a37e2 commit 690a4c2

File tree

5 files changed

+196
-37
lines changed

5 files changed

+196
-37
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
project = "sunpy-sphinx-theme test docs"
2020
author = "The SunPy Community"
21-
copyright = f"{datetime.datetime.now(datetime.UTC).year}, {author}" # NOQA: A001
21+
copyright = f"{datetime.datetime.now(datetime.timezone.utc).year}, {author}" # NOQA: A001
2222
extensions = [
2323
"sphinx_automodapi.automodapi",
2424
"sphinx_automodapi.smart_resolver",

src/sunpy_sphinx_theme/__init__.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,23 @@ def default_navbar():
5353
("sunpy-soar", "https://docs.sunpy.org/projects/soar/", 3),
5454
("sunraster", "https://docs.sunpy.org/projects/sunraster/", 3),
5555
("xrtpy", "https://xrtpy.readthedocs.io/", 3),
56-
# Then we have provisional packages
57-
("pyflct", "https://pyflct.readthedocs.io/", 3),
58-
("radiospectra", "https://docs.sunpy.org/projects/radiospectra/", 3),
59-
# These are tools which are not affiliated but are maintained by SunPy
60-
("ablog", "https://ablog.readthedocs.io/", 3),
61-
("mpl-animators", "https://docs.sunpy.org/projects/mpl-animators/", 3),
62-
("streamtracer", "https://docs.sunpy.org/projects/streamtracer/", 3),
56+
# Provisional packages submenu
57+
(
58+
"Provisional",
59+
[
60+
("pyflct", "https://pyflct.readthedocs.io/", 3),
61+
("radiospectra", "https://docs.sunpy.org/projects/radiospectra/", 3),
62+
],
63+
),
64+
# Tools submenu
65+
(
66+
"Tools",
67+
[
68+
("ablog", "https://ablog.readthedocs.io/en/stable/", 3),
69+
("mpl-animators", "https://docs.sunpy.org/projects/mpl-animators/", 3),
70+
("streamtracer", "https://docs.sunpy.org/projects/streamtracer/", 3),
71+
],
72+
),
6373
],
6474
),
6575
("Packages", "affiliated/", 2),
@@ -182,6 +192,10 @@ def setup(app: Sphinx):
182192
"https://gc.zgo.at/count.js",
183193
loading_method="async",
184194
)
195+
app.add_js_file(
196+
"js/submenu-concertina-toggle.js",
197+
loading_method="async",
198+
)
185199

186200
return {
187201
"parallel_read_safe": True,
Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,75 @@
1-
{# When we are rendering the top nav bar (wide screens) we use bootstrap dropdowns, #}
2-
{# when we render the sidebar (narrow screens) use collapse instead. #}
3-
{# TODO: If you have a section uncollapsed and then expand the width of your screen, weird shit happens #}
4-
{% if in_header %}
5-
{% set toggle="dropdown" %}
6-
{% set list_class="dropdown-menu" %}
7-
{% else %}
8-
{% set toggle="collapse" %}
9-
{% set list_class="collapse" %}
10-
{% endif %}
1+
{% macro render_nav_item(item, depth=0) %}
2+
{# Determine item type and a unique, context-prefixed submenu ID #}
3+
{% set is_leaf = item[1] is string %}
4+
{% set is_nested = item[1] is iterable and (item[1]|length > 0) and (item[1][0] is sequence or item[1][0] is mapping) %}
5+
{% set context_prefix = 'header' if in_header else 'sidebar' %}
6+
{% set base_id = item[0]|replace(" ", "_")|lower %}
7+
{% set submenu_id = context_prefix ~ '_' ~ base_id ~ '_' ~ depth %}
8+
9+
{# ----------------------------- LEAF LINK ----------------------------- #}
10+
{% if is_leaf %}
11+
<li class="nav-item{% if depth > 0 %} dropdown-submenu{% endif %} {% if depth == 0 %}ms-2{% endif %}">
12+
<a class="{% if depth == 0 %}nav-link{% else %}dropdown-item{% endif %}{% if depth > 1 %} concertina-subitem{% endif %}" href="{{ sst_pathto(*item[1:]) }}">
13+
{{ item[0] }}
14+
</a>
15+
</li>
16+
17+
{# ----------------------------- TOP LEVEL DROPDOWN ----------------------------- #}
18+
{% elif depth == 0 and is_nested %}
19+
<li class="nav-item dropdown ms-2">
20+
{% if in_header %}
21+
<a href="#" class="dropdown-toggle nav-link" id="dropdownMenu_{{ submenu_id }}"
22+
data-bs-toggle="dropdown" role="button" aria-expanded="false">
23+
{{ item[0] }}
24+
</a>
25+
<ul class="dropdown-menu" aria-labelledby="dropdownMenu_{{ submenu_id }}">
26+
{% else %}
27+
<a href="#" class="nav-link d-flex justify-content-between align-items-center"
28+
data-bs-toggle="collapse" data-bs-target="#collapse_{{ submenu_id }}"
29+
role="button" aria-expanded="false" aria-controls="collapse_{{ submenu_id }}">
30+
{{ item[0] }}
31+
<span class="caret" aria-hidden="true"></span>
32+
</a>
33+
<ul class="collapse" id="collapse_{{ submenu_id }}">
34+
{% endif %}
35+
{% for subitem in item[1] %}
36+
{{ render_nav_item(subitem, depth + 1) }}
37+
{% endfor %}
38+
</ul>
39+
</li>
40+
41+
{# ----------------------------- NESTED CONCERTINA ----------------------------- #}
42+
{% elif depth > 0 and is_nested %}
43+
<li>
44+
<button class="dropdown-item concertina-toggle d-flex align-items-center px-0 position-relative"
45+
aria-expanded="false"
46+
aria-controls="submenu_{{ submenu_id }}">
47+
<span class="concertina-label ps-4">{{ item[0] }}</span>
48+
<span class="concertina-icon position-absolute start-0" aria-hidden="true"></span>
49+
</button>
50+
<ul id="submenu_{{ submenu_id }}" class="concertina-submenu collapse">
51+
{% for subitem in item[1] %}
52+
{{ render_nav_item(subitem, depth + 1) }}
53+
{% endfor %}
54+
</ul>
55+
</li>
56+
{% endif %}
57+
{% endmacro %}
1158

1259
<nav class="navbar-nav">
1360
<ul class="bd-navbar-elements navbar-nav">
1461
{% if theme_navbar_links %}
15-
{%- for navlink in theme_navbar_links %}
16-
{% if navlink[1] is not string %}
17-
<li class="nav-item dropdown ms-2 has-children">
18-
<a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownMenuLink" data-bs-toggle="{{ toggle }}" data-bs-target="#{{ navlink[0] }}" aria-haspopup="true" aria-expanded="false">{{ navlink[0] }}<b class="caret"></b></a>
19-
<ul class="{{ list_class }}" id="{{ navlink[0] }}" aria-labelledby="dropdownMenuLink">
20-
{%- for link in navlink[1] %}
21-
<li class="nav-item">
22-
<a class="nav-link" href="{{ sst_pathto(*link[1:]) }}">{{ link[0] }}</a>
23-
</li>
24-
{%- endfor %}
25-
</ul>
26-
</li>
27-
{% else %}
28-
<li class="nav-item ms-2"><a class="nav-link" role="button" href="{{ sst_pathto(*navlink[1:]) }}">{{ navlink[0] }}</a></li>
29-
{% endif %}
30-
{%- endfor %}
62+
{% for navlink in theme_navbar_links %}
63+
{{ render_nav_item(navlink) }}
64+
{% endfor %}
3165
{% endif %}
66+
3267
{% if theme_external_links %}
33-
{%- for external_link in theme_external_links %}
34-
<li class="nav-item ms-2"><a class="nav-link nav-external" role="button" href="{{ external_link['url'] }}">{{ external_link['name'] }}</a></li>
35-
{%- endfor %}
68+
{% for ext in theme_external_links %}
69+
<li class="nav-item ms-2">
70+
<a class="nav-link nav-external" href="{{ ext['url'] }}">{{ ext['name'] }}</a>
71+
</li>
72+
{% endfor %}
3673
{% endif %}
3774
</ul>
3875
</nav>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
document.addEventListener("DOMContentLoaded", function () {
2+
// Handle concertina toggle clicks inside dropdown
3+
document.querySelectorAll(".concertina-toggle").forEach(function (toggle) {
4+
toggle.addEventListener("click", function (e) {
5+
e.preventDefault();
6+
e.stopPropagation();
7+
8+
const submenuId = toggle.getAttribute("aria-controls");
9+
const submenu = document.getElementById(submenuId);
10+
if (!submenu) return;
11+
submenu.classList.add("collapse"); // Remove bootstraps control to ensure we can toggle
12+
const expanded = submenu.classList.contains("show");
13+
submenu.classList.toggle("show", !expanded);
14+
submenu.classList.toggle("collapse", !expanded);
15+
toggle.setAttribute("aria-expanded", String(!expanded));
16+
});
17+
});
18+
19+
// Prevent dropdown from closing when clicking inside the concertina
20+
document.querySelectorAll(".dropdown-menu").forEach(function (menu) {
21+
menu.addEventListener("click", function (e) {
22+
if (
23+
e.target.closest(".concertina-toggle") ||
24+
e.target.closest(".concertina-submenu")
25+
) {
26+
e.stopPropagation();
27+
}
28+
});
29+
});
30+
31+
// Reset all concertinas when dropdown is hidden
32+
document.querySelectorAll(".dropdown").forEach(function (dropdown) {
33+
dropdown.addEventListener("hide.bs.dropdown", function () {
34+
dropdown
35+
.querySelectorAll(".concertina-submenu.show")
36+
.forEach(function (submenu) {
37+
submenu.classList.remove("show");
38+
});
39+
dropdown
40+
.querySelectorAll(".concertina-toggle")
41+
.forEach(function (toggle) {
42+
toggle.setAttribute("aria-expanded", "false");
43+
});
44+
});
45+
});
46+
});

src/sunpy_sphinx_theme/theme/sunpy/static/sunpy_style.css

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,3 +412,65 @@ html[data-theme="dark"] .search-button-field:hover {
412412
align-items: center;
413413
}
414414
}
415+
416+
.concertina-icon {
417+
left: 0;
418+
transition: transform 0.3s ease;
419+
pointer-events: none;
420+
text-decoration: none !important; /* otherwise couldn't get the icon to not be underlined */
421+
}
422+
423+
.concertina-toggle[aria-expanded="true"] .concertina-icon {
424+
transform: rotate(90deg);
425+
}
426+
427+
.concertina-submenu {
428+
list-style: none;
429+
margin: 0;
430+
padding: 0;
431+
display: none; /* Controlled via the concertina toggle JS */
432+
}
433+
434+
.concertina-submenu.show {
435+
display: block;
436+
}
437+
.concertina-submenu.collapse {
438+
width: 100%;
439+
}
440+
441+
.concertina-subitem {
442+
padding-left: 2rem;
443+
}
444+
445+
/* Have to restore some pst styling since we have removed some bs classes in the navbar_center template */
446+
.navbar-nav .concertina-toggle {
447+
color: var(--pst-color-text-muted);
448+
}
449+
.bd-header .dropdown-item,
450+
.bd-header .concertina-toggle {
451+
color: var(--sst-header-text);
452+
}
453+
454+
/* Hover and focus styles */
455+
.concertina-toggle.dropdown-item:hover,
456+
.navbar-nav .concertina-toggle:hover,
457+
.bd-header .dropdown-item:hover,
458+
.bd-header .concertina-toggle:hover {
459+
color: var(--pst-color-link-hover);
460+
background-color: transparent;
461+
text-decoration: underline;
462+
}
463+
464+
.navbar-nav .concertina-toggle:hover {
465+
text-decoration-thickness: max(3px, 0.1875rem, 0.12em);
466+
text-underline-offset: 0.1578em;
467+
}
468+
.bd-header .concertina-toggle .concertina-icon {
469+
left: 0.4rem !important;
470+
}
471+
/* Limit the height of dropdown menu to keep on screen */
472+
.bd-header .dropdown-menu {
473+
max-height: 80vh;
474+
overflow-y: auto;
475+
overscroll-behavior: contain;
476+
}

0 commit comments

Comments
 (0)