Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 19 additions & 1 deletion src/pydata_sphinx_theme/toctree.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ class LinkInfo:
href: str
title: str
is_external: bool
# `is_current` is true for the whole section the reader is inside, which is what
# drives the visual highlight. `is_current_page` is true only on the section's own
# landing page, which is the narrower condition `aria-current="page"` describes.
is_current_page: bool = False


def add_toctree_functions(
Expand Down Expand Up @@ -193,6 +197,7 @@ def _generate_nav_info() -> list[LinkInfo]:
links_data.append(
LinkInfo(
is_current=(page == active_header_page),
is_current_page=(page == pagename),
href=link_href,
title=title,
is_external=is_absolute,
Expand Down Expand Up @@ -238,7 +243,7 @@ def _generate_header_nav_before_dropdown(
boilerplate = dedent(
"""
<li class="{nav_item} {active}">
<a class="{nav_link} nav-{ext_int}" href="{href}">
<a class="{nav_link} nav-{ext_int}" href="{href}"{aria_current}>
{title}
</a>
</li>
Expand All @@ -258,6 +263,7 @@ def _generate_header_nav_before_dropdown(
links_html.append(
boilerplate.format(
active="current active" if link.is_current else "",
aria_current=' aria-current="page"' if link.is_current_page else "",
nav_link=nav_link,
nav_item=nav_item,
ext_int="external" if link.is_external else "internal",
Expand All @@ -269,6 +275,7 @@ def _generate_header_nav_before_dropdown(
links_dropdown.append(
boilerplate.format(
active="current active" if link.is_current else "",
aria_current=' aria-current="page"' if link.is_current_page else "",
nav_link=nav_link + " " + dropdown_item,
nav_item="",
ext_int="external" if link.is_external else "internal",
Expand Down Expand Up @@ -409,6 +416,12 @@ def generate_toctree_html(
for li in soup("li", {"class": "current"}):
li["class"].append("active")

# Sphinx puts the "current" class on every ancestor <li> but only on the
# anchor of the page actually being rendered, so this marks that one page
# and not the section it sits under.
for anchor in soup("a", {"class": "current"}):
anchor["aria-current"] = "page"

# Remove sidebar links to sub-headers on the page
for li in soup.select("li"):
# Remove
Expand Down Expand Up @@ -620,11 +633,16 @@ def _move_current_markers(
for anchor in soup.find_all("a", href="#"):
anchor["href"] = old_href
anchor["class"] = [cls for cls in anchor.get("class", []) if cls != "current"]
# `aria-current` has to move with the class it mirrors. A cached sidebar
# that kept it would announce the wrong page as the current one, which is
# worse than not marking a current page at all.
del anchor["aria-current"]
_set_current_chain(anchor, current=False, show_nav_level=show_nav_level)
# Promote this page's entry
for anchor in new_anchors:
anchor["href"] = "#"
anchor["class"] = ["current", *anchor.get("class", [])]
anchor["aria-current"] = "page"
_set_current_chain(anchor, current=True, show_nav_level=show_nav_level)
return True

Expand Down
40 changes: 40 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,46 @@ def test_sidebars_nested_page(sphinx_build_factory, file_regression) -> None:
file_regression.check(sidebar.prettify(), extension=".html")


def test_aria_current_marks_only_the_current_page(sphinx_build_factory) -> None:
"""The current page is marked in the accessibility tree, not just visually.

Sphinx marks the reader's position with a ``current`` class, which is
invisible to assistive technology. ``aria-current="page"`` has to say the
same thing, and it has to be narrower than the class: the class highlights
the whole section the reader is inside, while ``aria-current="page"`` may
only ever be on the one page being viewed.
"""
sphinx_build = sphinx_build_factory("sidebars").build()

page_html = sphinx_build.html_tree("section1/subsection1/page1.html")
sidebar = page_html.select("nav.bd-docs-nav")[0]

marked = sidebar.select('a[aria-current="page"]')
assert len(marked) == 1, "exactly one sidebar entry may be the current page"
# A toctree renders the current page's own entry as a self-link
assert marked[0]["href"] == "#"
assert "current" in marked[0]["class"]

# The ancestors of the current page are highlighted with the `current`
# class, and must not claim to be the current page themselves.
for li in sidebar.select("li.current"):
for ancestor_link in li.select("a"):
if ancestor_link is marked[0]:
continue
assert ancestor_link.get("aria-current") is None

# The header nav marks a top-level entry only when that page is the one
# being viewed, not when the reader is merely somewhere beneath it.
navbar = page_html.select("ul.bd-navbar-elements")[0]
assert not navbar.select("a[aria-current]"), (
"a section the reader is inside is not the current page"
)

landing_html = sphinx_build.html_tree("section1/index.html")
landing_navbar = landing_html.select("ul.bd-navbar-elements")[0]
assert len(landing_navbar.select('a[aria-current="page"]')) == 1


def test_sidebars_level2(sphinx_build_factory, file_regression) -> None:
"""Test sidebars in a second-level page w/ children."""
confoverrides = {
Expand Down
2 changes: 1 addition & 1 deletion tests/test_build/math_header_item.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<li class="nav-item current active">
<a class="nav-link nav-internal" href="#">
<a aria-current="page" class="nav-link nav-internal" href="#">
Page
<span class="math notranslate nohighlight">
\(\beta\)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_build/sidebar_subpage.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</a>
</li>
<li class="nav-item current active">
<a class="nav-link nav-internal" href="#">
<a aria-current="page" class="nav-link nav-internal" href="#">
Section 1 index
</a>
</li>
Expand Down
2 changes: 1 addition & 1 deletion tests/test_build/test_sidebars_nested_page.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</summary>
<ul class="current">
<li class="toctree-l2 current active">
<a class="current reference internal" href="#">
<a aria-current="page" class="current reference internal" href="#">
Section 1 sub 1 page 1
</a>
</li>
Expand Down
Loading