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 18 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
4 changes: 3 additions & 1 deletion .github/workflows/ci_cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ 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@54d45960ae9d009b2a3f26dfa687f611a85c6da3 # v10.0.11
uses: ansys/actions/build-wheelhouse@v9
with:
library-name: ${{ env.PACKAGE_NAME }}
operating-system: ${{ matrix.os }}
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
11 changes: 4 additions & 7 deletions doc/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
import os
from pathlib import Path
import subprocess
from typing import Dict, List
from typing import List

from github import Github
import plotly.io as pio
import pyvista
import requests
from sphinx.application import Sphinx
from sphinx.builders.latex import LaTeXBuilder

from ansys_sphinx_theme import (
Expand Down Expand Up @@ -82,6 +81,9 @@
"Examples": ["examples/"],
"Contributing": ["contribute/"],
},
"use_navigation_dropdown": {
"navigation_yaml_file": "navbar.yml",
},
}


Expand Down Expand Up @@ -334,8 +336,3 @@ def revert_exclude_patterns(app, env):
excluded_pattern = env.config.exclude_patterns
excluded_pattern.remove("examples/gallery-examples/*.ipynb")
env.config.exclude_patterns = excluded_pattern


def setup(app: Sphinx) -> Dict:
"""Sphinx hooks to add to the setup."""
app.connect("env-updated", revert_exclude_patterns)
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.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
- 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
46 changes: 45 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,50 @@

.. 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
------------------------
Ansys sphinx theme now supports the navigation bar dropdowns for the header links.
To enable the navigation bar dropdowns, add the following dictionary to the ``html_theme_options`` dictionary:

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

.. code:: python

html_theme_options = {
"use_navigation_dropdown": {
"navigation_yaml_file": "navbar.yml",
}

The theme looks for the ``navbar.yml`` file in the ``doc/source`` directory.
The YAML file should contain the following structure:

.. 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


- file: The path to the file relative to the ``doc/source`` directory.
- title: The title of the navigation item.
- sections: A list of sections under the navigation item. Each section can have its own file
and title.
- caption: An optional caption for the section.

.. warning::

If you enable the navigation bar dropdowns, you must add the documents you wish to include in the
header sections to the ``navbar.yml`` file. If you do not add the documents, they will not be displayed in the

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

View workflow job for this annotation

GitHub Actions / vale

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

[Google.Will] Avoid using 'will'.
Raw output
{"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "doc/source/user-guide/options.rst", "range": {"start": {"line": 442, "column": 87}}}, "severity": "WARNING"}
navigation bar dropdowns.
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
176 changes: 176 additions & 0 deletions src/ansys_sphinx_theme/navbar_dropdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# 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
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import PythonLexer
import sphinx
from sphinx.util.nodes import make_refnode
import yaml


def load_navbar_configuration(app: sphinx.application.Sphinx) -> None:
"""Load the navbar configuration from a YAML file for the Sphinx app."""
config_options = app.config.html_theme_options.get("use_navigation_dropdown", {})
if not config_options:
return
navigation_yaml_file = config_options.get("navigation_yaml_file", None)
print(f"Loading navbar configuration from: {navigation_yaml_file}")
if navigation_yaml_file:
try:
with pathlib.Path.open(app.srcdir / navigation_yaml_file, encoding="utf-8") as f:
app.config.navbar_contents = yaml.safe_load(f)
except FileNotFoundError:
raise FileNotFoundError(
f"Navbar configuration file '{navigation_yaml_file}' not found in source directory."
)
except yaml.YAMLError as exc:
raise ValueError(f"Error parsing YAML file '{navigation_yaml_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.



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:
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
context["pygments_highlight_python"] = lambda code: highlight(
code, PythonLexer(), HtmlFormatter()
)


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


def render_example_gallery_dropdown(example_enum: type) -> bs4.BeautifulSoup:
"""Render a dropdown menu for filtering example gallery items."""
soup = bs4.BeautifulSoup()
dropdown_name = example_enum.formatted_name().lower().replace(" ", "-")
dropdown_container = soup.new_tag(
"div", attrs={"class": "filter-dropdown", "id": f"{dropdown_name}-dropdown"}
)
dropdown_show_checkbox = soup.new_tag(
"input",
attrs={
"class": "dropdown-checkbox",
"id": f"{dropdown_name}-checkbox",
"type": "checkbox",
},
)
dropdown_container.append(dropdown_show_checkbox)
dropdown_label = soup.new_tag(
"label", attrs={"class": "dropdown-label", "for": f"{dropdown_name}-checkbox"}
)
dropdown_label.append(example_enum.formatted_name())
chevron = soup.new_tag("i", attrs={"class": "fa-solid fa-chevron-down"})
dropdown_label.append(chevron)
dropdown_container.append(dropdown_label)
if example_enum.values():
dropdown_options = soup.new_tag("div", attrs={"class": "dropdown-content"})
for member in list(example_enum):
label = soup.new_tag("label", attrs={"class": "checkbox-container"})
label.append(member.value)
tag = getattr(member, "tag", member.value)
checkbox = soup.new_tag(
"input",
attrs={
"id": f"{tag}-checkbox",
"class": "filter-checkbox",
"type": "checkbox",
},
)
label.append(checkbox)
checkmark = soup.new_tag("span", attrs={"class": "checkmark"})
label.append(checkmark)
dropdown_options.append(label)
dropdown_container.append(dropdown_options)
soup.append(dropdown_container)
return soup
Loading
Loading