Skip to content

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

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7ea985a
feat: navigation bar dropdown
Revathyvenugopal162 May 23, 2025
f7b238c
feat: navigation bars
Revathyvenugopal162 Jun 5, 2025
9e42940
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jun 5, 2025
f39c7eb
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jun 9, 2025
0b72e44
chore: adding changelog file 723.added.md [dependabot-skip]
pyansys-ci-bot Jun 9, 2025
185f9a0
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jun 13, 2025
2e32424
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jun 16, 2025
2870ba6
Merge branch 'feat/navbar-dropdown' of https://github.com/ansys/ansys…
Revathyvenugopal162 Jun 16, 2025
b973378
feat: add styles
Revathyvenugopal162 Jun 18, 2025
09bfde1
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jun 18, 2025
9fc57a6
fix: extends the navbar
Revathyvenugopal162 Jun 18, 2025
e03a803
fix: smoketest
Revathyvenugopal162 Jun 18, 2025
2b9ff09
test: actions v9
Revathyvenugopal162 Jun 18, 2025
52cb5cc
fix: cleanup code
Revathyvenugopal162 Jun 19, 2025
7cb9ba5
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jun 23, 2025
00310a5
feat: update the docs
Revathyvenugopal162 Jun 26, 2025
2e7156a
Apply suggestions from code review
Revathyvenugopal162 Jun 26, 2025
6332c3c
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jun 27, 2025
eca07c3
fix: revert the hooks changes
Revathyvenugopal162 Jun 27, 2025
adf8061
Apply suggestions from code review
Revathyvenugopal162 Jun 27, 2025
94589a4
fix: code style
Revathyvenugopal162 Jun 27, 2025
867066e
Apply suggestions from code review
Revathyvenugopal162 Jun 27, 2025
4edc176
fix: rename navigathion theme option
Revathyvenugopal162 Jun 27, 2025
4d75e29
Merge branch 'feat/navbar-dropdown' of https://github.com/ansys/ansys…
Revathyvenugopal162 Jun 27, 2025
f5d46b5
fix(style) update the navbar
Revathyvenugopal162 Jun 27, 2025
42ca1d7
docs(docstring): add docs for navbar
Revathyvenugopal162 Jun 27, 2025
42ef786
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jun 30, 2025
7de6d62
fix: remove the unused code
Revathyvenugopal162 Jun 30, 2025
289f9e6
Merge branch 'feat/navbar-dropdown' of https://github.com/ansys/ansys…
Revathyvenugopal162 Jun 30, 2025
e041fc1
Update src/ansys_sphinx_theme/navbar_dropdown.py
Revathyvenugopal162 Jun 30, 2025
ef2c00f
fix(style): precommit
Revathyvenugopal162 Jun 30, 2025
8cd2a61
fix(log): add logging
Revathyvenugopal162 Jun 30, 2025
08e60c1
fix: update the navbar
Revathyvenugopal162 Jun 30, 2025
fe14d05
fix: update dependency
Revathyvenugopal162 Jun 30, 2025
1ff44b9
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jul 1, 2025
d9b2de7
Merge branch 'main' into feat/navbar-dropdown
Revathyvenugopal162 Jul 1, 2025
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 .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ jobs:
- should-release: false
os: macos-latest
steps:
- name: "Install Git and clone project"
uses: actions/checkout@v4
- name: "Build wheelhouse and perform smoke test"
uses: ansys/actions/build-wheelhouse@8d3e4946f36c2a7d447b92e34b1022a5c9dc77a7 # v10.0.12
with:
Expand Down
1 change: 1 addition & 0 deletions doc/changelog.d/723.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dropdown for navigation bar
3 changes: 3 additions & 0 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
"Examples": ["examples/"],
"Contributing": ["contribute/"],
},
"navigation_dropdown": {
"layout_file": "navbar.yml",
},
}


Expand Down
26 changes: 26 additions & 0 deletions doc/source/navbar.yml
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
47 changes: 46 additions & 1 deletion doc/source/user-guide/options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@

print("hello world")

If a format is used in the "content" field that does not fall into the categories above, it will not

Check warning on line 347 in doc/source/user-guide/options.rst

View workflow job for this annotation

GitHub Actions / vale

[vale] doc/source/user-guide/options.rst#L347

[Google.Will] Avoid using 'will'.
Raw output
{"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "doc/source/user-guide/options.rst", "range": {"start": {"line": 347, "column": 93}}}, "severity": "WARNING"}
be rendered correctly.

To enable the "What's new" sections and sidebar in the changelog file, add the following dictionary
Expand All @@ -367,9 +367,9 @@
The dictionary contains the following keys:

- ``whatsnew_file_path``: The path to the YAML file containing what's new content local to the
``doc/source`` directory. If not provided, the what's new section will not be generated.

Check warning on line 370 in doc/source/user-guide/options.rst

View workflow job for this annotation

GitHub Actions / vale

[vale] doc/source/user-guide/options.rst#L370

[Google.Will] Avoid using 'will'.
Raw output
{"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "doc/source/user-guide/options.rst", "range": {"start": {"line": 370, "column": 69}}}, "severity": "WARNING"}
- ``changelog_file_path``: The path to the changelog.rst file local to the ``doc/source``
directory. If not provided, the what's new section will not be generated.

Check warning on line 372 in doc/source/user-guide/options.rst

View workflow job for this annotation

GitHub Actions / vale

[vale] doc/source/user-guide/options.rst#L372

[Google.Will] Avoid using 'will'.
Raw output
{"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "doc/source/user-guide/options.rst", "range": {"start": {"line": 372, "column": 54}}}, "severity": "WARNING"}
- ``sidebar_pages``: List of names for the pages to include the what's new sidebar on. If not
provided, the what's new sidebar is not displayed.
- ``sidebar_no_of_headers``: Number of minor version sections to display in the what's new sidebar.
Expand All @@ -394,6 +394,51 @@

.. note::

If you are using both the "whatsnew" and "cheatsheet" options, the "cheatsheet" option will be

Check warning on line 397 in doc/source/user-guide/options.rst

View workflow job for this annotation

GitHub Actions / vale

[vale] doc/source/user-guide/options.rst#L397

[Google.Will] Avoid using 'will'.
Raw output
{"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "doc/source/user-guide/options.rst", "range": {"start": {"line": 397, "column": 92}}}, "severity": "WARNING"}
displayed first in the left navigation pane, followed by the "What's new" section to maintain
sidebar consistency.
sidebar consistency.

Navigation bar dropdown
------------------------
This theme supports dropdown navigation bars. The layout is declared using a YAML file contained at any level in the ``doc/source`` directory. This file must be specified in the ``html_theme_options`` so Sphinx can apply the desired navigation structure:

- ``navigation_yaml_file``: The path to the YAML file containing the navigation structure.

.. code:: python

html_theme_options = {
...,
"navigation_dropdown": {
"layout_file": "navbar.yml",
},
}

Each entry in the YAML file may include the following fields:

- **file.** The relative path to the documentation file, based on the doc/source directory.

- **title.** The text displayed for the link in the dropdown navigation menu.

- **sections.** A list of nested navigation items. Each section can specify its own file, title, and an optional caption to provide a brief description.

.. code:: yaml

- file: api/index
title: "API Reference"

- file: examples
title: "Examples"
sections:

- file: examples/sphinx-design.rst
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


.. warning::

You must declare the complete layout of the dropdown navigation bar in the YAML file. Sphinx does not resolve it automatically.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies = [
"Jinja2>=3.1.2",
"importlib-metadata>=4.0",
"pdf2image>=1.17.0",
"PyYAML==6.0.2",
]

[project.optional-dependencies]
Expand Down
4 changes: 4 additions & 0 deletions src/ansys_sphinx_theme/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from ansys_sphinx_theme.cheatsheet import build_quarto_cheatsheet, cheatsheet_sidebar_pages
from ansys_sphinx_theme.extension.linkcode import DOMAIN_KEYS, sphinx_linkcode_resolve
from ansys_sphinx_theme.latex import generate_404
from ansys_sphinx_theme.navbar_dropdown import load_navbar_configuration, update_template_context
from ansys_sphinx_theme.search import (
create_search_index,
update_search_config,
Expand Down Expand Up @@ -494,6 +495,7 @@ def setup(app: Sphinx) -> Dict:

# Add default HTML configuration
setup_default_html_theme_options(app)
load_navbar_configuration(app)

# Check for what's new options in the theme configuration
whatsnew_file, changelog_file = get_whatsnew_options(app)
Expand All @@ -520,10 +522,12 @@ def setup(app: Sphinx) -> Dict:
app.connect("html-page-context", fix_edit_html_page_context)
app.connect("html-page-context", append_og_site_name)
app.connect("html-page-context", update_search_sidebar_context)
app.connect("html-page-context", update_template_context)

app.connect("build-finished", replace_html_tag)
if use_ansys_search:
app.connect("build-finished", create_search_index)

return {
"version": __version__,
"parallel_read_safe": True,
Expand Down
135 changes: 135 additions & 0 deletions src/ansys_sphinx_theme/navbar_dropdown.py
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"]]]
Copy link
Member

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.

"""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
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"}))
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
I wonder if you have set an specific class name for the items in each dropdown, so we can do something similar (or use bs4 easily on them).

Copy link
Contributor Author

@Revathyvenugopal162 Revathyvenugopal162 Jul 1, 2025

Choose a reason for hiding this comment

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

yes, We have the specific container for navbar-dropdown as you can see in the line L#131.

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
Expand Up @@ -22,3 +22,4 @@ whatsnew =
use_ansys_search = True
search_extra_sources =
search_filters =
navigation_dropdown =
Loading