-
Notifications
You must be signed in to change notification settings - Fork 8
feat: dropdown for navigation bar #723
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
base: main
Are you sure you want to change the base?
Changes from all commits
7ea985a
f7b238c
9e42940
f39c7eb
0b72e44
185f9a0
2e32424
2870ba6
b973378
09bfde1
9fc57a6
e03a803
2b9ff09
52cb5cc
7cb9ba5
00310a5
2e7156a
6332c3c
eca07c3
adf8061
94589a4
867066e
4edc176
4d75e29
f5d46b5
42ca1d7
42ef786
7de6d62
289f9e6
e041fc1
ef2c00f
8cd2a61
08e60c1
fe14d05
1ff44b9
d9b2de7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Dropdown for navigation bar |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
- file: getting-started | ||
title: "Get Started" | ||
- file: user-guide | ||
title: "Use Cases" | ||
- file: api/index | ||
title: "API Reference" | ||
- file: examples | ||
title: "Examples" | ||
sections: | ||
- file: examples/sphinx-design | ||
title: "Sphinx Design Examples" | ||
caption: Examples of using Sphinx design features | ||
- file: examples/nbsphinx | ||
title: "Nbsphinx Examples" | ||
caption: Examples of using Nbsphinx for Jupyter Notebooks | ||
- file: changelog | ||
title: "New Features" | ||
- file: contribute | ||
title: "Contribute" | ||
sections: | ||
- file: contribute/developer | ||
title: developer contributions | ||
caption: Contribute as a developer | ||
- file: contribute/user | ||
title: user contributions | ||
caption: Contribute as a user |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates. | ||
# SPDX-License-Identifier: MIT | ||
# | ||
# | ||
# Permission is hereby granted, free of charge, to any person obtaining a copy | ||
# of this software and associated documentation files (the "Software"), to deal | ||
# in the Software without restriction, including without limitation the rights | ||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
# copies of the Software, and to permit persons to whom the Software is | ||
# furnished to do so, subject to the following conditions: | ||
# | ||
# The above copyright notice and this permission notice shall be included in all | ||
# copies or substantial portions of the Software. | ||
# | ||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
# SOFTWARE. | ||
|
||
"""Navigation Dropdown for navigation bar.""" | ||
|
||
import copy | ||
from functools import lru_cache | ||
import pathlib | ||
from typing import Dict, List, Union | ||
|
||
import bs4 | ||
from docutils import nodes | ||
import sphinx | ||
from sphinx.util import logging | ||
from sphinx.util.nodes import make_refnode | ||
import yaml | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def load_navbar_configuration(app: sphinx.application.Sphinx) -> None: | ||
"""Load the navbar configuration from a YAML file for the Sphinx app.""" | ||
if not ( | ||
"navigation_dropdown" in app.config.html_theme_options | ||
and "layout_file" in app.config.html_theme_options["navigation_dropdown"] | ||
): | ||
return | ||
|
||
layout_file = app.config.html_theme_options["navigation_dropdown"].get("layout_file", None) | ||
try: | ||
with pathlib.Path.open(app.srcdir / layout_file, encoding="utf-8") as config_file: | ||
app.config.navbar_contents = yaml.safe_load(config_file) | ||
except FileNotFoundError: | ||
raise FileNotFoundError(f"Could not find {layout_file}.") | ||
except yaml.YAMLError as exc: | ||
raise ValueError(f"Error parsing '{layout_file}': {exc}") | ||
|
||
|
||
NavEntry = Dict[str, Union[str, List["NavEntry"]]] | ||
"""Type alias for a navigation entry in the navbar configuration. | ||
|
||
Each entry can have a 'file' or 'link' key, and optionally 'title', | ||
'caption', and 'sections' keys. The 'sections' key contains a list of | ||
sub-entries, allowing for nested navigation structures. | ||
""" | ||
|
||
|
||
def update_template_context(app, pagename, templatename, context, doctree): | ||
"""Inject custom variables and utilities into the Sphinx template context.""" | ||
|
||
@lru_cache(maxsize=None) | ||
def render_navbar_links_html() -> bs4.BeautifulSoup: | ||
"""Render external header links as HTML for the navbar.""" | ||
if not hasattr(app.config, "navbar_contents"): | ||
raise ValueError("Navbar configuration is missing. Please specify a navbar YAML file.") | ||
node = nodes.container(classes=["navbar-content"]) | ||
node.append(build_navbar_nodes(app.config.navbar_contents)) | ||
header_soup = bs4.BeautifulSoup(app.builder.render_partial(node)["fragment"], "html.parser") | ||
return add_navbar_chevrons(header_soup) | ||
|
||
def build_navbar_nodes(obj: List[NavEntry], is_top_level: bool = True) -> nodes.Node: | ||
"""Recursively build navbar nodes from configuration entries.""" | ||
bullet_list = nodes.bullet_list( | ||
bullet="-", | ||
classes=["navbar-toplevel" if is_top_level else "navbar-sublevel"], | ||
) | ||
for item in obj: | ||
if "file" in item: | ||
ref_node = make_refnode( | ||
app.builder, | ||
context["current_page_name"], | ||
item["file"], | ||
None, | ||
nodes.inline(classes=["navbar-link-title"], text=item.get("title")), | ||
item.get("title"), | ||
) | ||
elif "link" in item: | ||
ref_node = nodes.reference("", "", internal=False) | ||
ref_node["refuri"] = item.get("link") | ||
ref_node["reftitle"] = item.get("title") | ||
ref_node.append(nodes.inline(classes=["navbar-link-title"], text=item.get("title"))) | ||
else: | ||
logger.warning( | ||
f"Navbar entry '{item}' is missing 'file' or 'link' key. Skipping this entry." | ||
) | ||
continue | ||
germa89 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if "caption" in item: | ||
caption = nodes.Text(item.get("caption")) | ||
ref_node.append(caption) | ||
paragraph = nodes.paragraph() | ||
paragraph.append(ref_node) | ||
container = nodes.container(classes=["ref-container"]) | ||
container.append(paragraph) | ||
list_item = nodes.list_item( | ||
classes=["active-link"] if item.get("file") == pagename else [] | ||
) | ||
list_item.append(container) | ||
if "sections" in item: | ||
wrapper = nodes.container(classes=["navbar-dropdown"]) | ||
wrapper.append(build_navbar_nodes(item["sections"], is_top_level=False)) | ||
list_item.append(wrapper) | ||
bullet_list.append(list_item) | ||
return bullet_list | ||
|
||
context["render_navbar_links_html"] = render_navbar_links_html | ||
|
||
|
||
def add_navbar_chevrons(input_soup: bs4.BeautifulSoup) -> bs4.BeautifulSoup: | ||
"""Add dropdown chevron icons to navbar items with submenus.""" | ||
soup = copy.copy(input_soup) | ||
for li in soup.find_all("li", recursive=True): | ||
divs = li.find_all("div", {"class": "navbar-dropdown"}, recursive=False) | ||
if divs: | ||
ref = li.find("div", {"class": "ref-container"}) | ||
ref.append(soup.new_tag("i", attrs={"class": "fa-solid fa-chevron-down"})) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like that the chevrons have their class, so it is easier to customise it through CSS files. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, We have the specific container for |
||
return soup |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
{% if theme_navigation_dropdown %} | ||
<nav class="navbar-nav"> | ||
<p class="sidebar-header-items__title" | ||
role="heading" | ||
aria-level="1" | ||
aria-label="{{ _('Site Navigation') }}"> | ||
<!-- {{ _("Site Navigation") }} --> | ||
</p> | ||
{{ render_navbar_links_html() }} | ||
</nav> | ||
<style> | ||
/* Top navbar styling */ | ||
.navbar-toplevel p { | ||
margin: 0; | ||
padding-inline-start: 0; | ||
} | ||
div.navbar-dropdown { | ||
display: none; | ||
position: relative; | ||
left: -100%; | ||
margin-top: 2rem; | ||
} | ||
.navbar-sublevel p a.reference { | ||
text-decoration: none; | ||
} | ||
.navbar-sublevel p a.reference:hover > span.navbar-link-title { | ||
text-decoration: underline; | ||
color: var(--pst-color-link-hover); | ||
} | ||
.navbar-toplevel li { | ||
display: inline-flex; | ||
justify-content: center; | ||
align-items: center; | ||
height: 100%; | ||
padding: 0em 1em; | ||
|
||
} | ||
|
||
ul.navbar-toplevel li:hover > div.navbar-dropdown { | ||
display: block; | ||
} | ||
|
||
.navbar-content ul.navbar-sublevel { | ||
position: absolute; | ||
gap: 1rem; | ||
padding: 1em 1em; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: baseline; | ||
background-color: white; | ||
white-space: pre; | ||
box-shadow: 0 5px 15px 0 rgb(0 0 0 / 10%); | ||
} | ||
|
||
div.navbar-content a { | ||
display: flex; | ||
flex-direction: column; | ||
color: var(--ast-navbar-color); | ||
align-items: baseline; | ||
justify-content: center; | ||
font-weight: var(--ast-font-weight-semibold); | ||
} | ||
.ref-container { | ||
display: flex; | ||
flex-direction: row; | ||
gap: 0.5em; | ||
font-family: var(--ast-body-family); | ||
line-height: var(--ast-global-line-height); | ||
align-items: center; | ||
justify-content: center; | ||
height: 100%; | ||
} | ||
/* Highlight active nav bar link */ | ||
li.active-link { | ||
font-weight: bold; | ||
} | ||
/* Set the first .navbar-persistent--mobile element to have auto left margin */ | ||
/* Disable underline for hovered links in the nav bar */ | ||
.navbar-nav li a:hover { | ||
text-decoration: none; | ||
} | ||
.navbar-header-items { | ||
padding-left: 0; | ||
} | ||
</style> | ||
{% else %} | ||
{% include "pydata_sphinx_theme/components/navbar-nav.html" %} | ||
{% endif %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,3 +22,4 @@ whatsnew = | |
use_ansys_search = True | ||
search_extra_sources = | ||
search_filters = | ||
navigation_dropdown = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a docstring for this so we remember its goal.