Skip to content

Commit c4a672b

Browse files
gabalafoudrammocktrallard
authored
Make TOC sections expandable and collapsible by keyboard (#1582)
* Consistent focus ring (first pass) (#1549) * wip * Style focus state in header nav * update focus ring style on all focussable elements * simplify * fix links in mobile sidebar overlay * put focus rings around a few more focusable elements * polish * update comment * review * better align focus ring on collapsible admonitions * comment and simplify sphinx-togglebutton focus ring * make css override more explicit * Fix SD link-card focus ring and on homepage, bring links inside card * Update docs/index.md --------- Co-authored-by: Daniel McCloy <[email protected]> * Resolve current sidebar link notch and focus ring (#1561) * Fix sidebar current notch * focus-ring-radius * missed a spot 0.125rem * keep focus ring on top * Restyle Sphinx Design tabs (#1555) * restyle sphinx design tabs * increase panel border radius * increase line height, zero padding-y * use shadow variable * Update src/pydata_sphinx_theme/assets/styles/extensions/_sphinx_design.scss * Update src/pydata_sphinx_theme/assets/styles/extensions/_sphinx_design.scss * Fix tabbed panel colors (#1567) * aria attributes do not update if user uses mouse instead of keyboard * details/summary, no checkbox * clean up * details["open"] * make it work for nav level 0 * pull link outside of details tag * make it work with show nav level 0 * make level 0 heading clickable * comments * restore .current notches to toc parents * fix tests * clean up * more comments * make toggle icon bigger at level 0 * comments * clarify comment * remove list-captions class from in-page toc styles * Update tests/test_build.py * Update src/pydata_sphinx_theme/toctree.py Co-authored-by: Tania Allard <[email protected]> * update test for verbose HTML boolean attribute * more HTML boolean attribute notation updates * Update comment about link vs non-link HTML structure --------- Co-authored-by: Daniel McCloy <[email protected]> Co-authored-by: Tania Allard <[email protected]>
1 parent c05fa05 commit c4a672b

File tree

7 files changed

+218
-151
lines changed

7 files changed

+218
-151
lines changed

src/pydata_sphinx_theme/assets/styles/components/_navbar-links.scss

-26
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,4 @@
2626
@include link-style-text;
2727
}
2828
}
29-
30-
/**
31-
* Togglable expand/collapse
32-
* This is only applicable to the primary sidebar which has these checkboxes
33-
*/
34-
.toctree-checkbox {
35-
position: absolute;
36-
display: none;
37-
}
38-
39-
.toctree-checkbox {
40-
~ ul {
41-
display: none;
42-
}
43-
~ label .fa-chevron-down {
44-
transform: rotate(0deg);
45-
}
46-
}
47-
.toctree-checkbox:checked {
48-
~ ul {
49-
display: block;
50-
}
51-
~ label .fa-chevron-down {
52-
transform: rotate(180deg);
53-
}
54-
}
5529
}

src/pydata_sphinx_theme/assets/styles/components/_toc-inpage.scss

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ nav.page-toc {
66
margin-bottom: 1rem;
77
}
88

9-
.bd-toc .nav,
10-
.list-caption {
9+
.bd-toc .nav {
1110
.nav {
1211
display: none;
1312

src/pydata_sphinx_theme/assets/styles/sections/_sidebar-primary.scss

+93-47
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* e.g., between-pages navigation.
44
*/
55

6+
$sidebar-padding-right: 1rem;
67
.bd-sidebar-primary {
78
display: flex;
89
flex-direction: column;
@@ -13,7 +14,7 @@
1314
@include make-col(3);
1415

1516
// Borders padding and whitespace
16-
padding: 2rem 1rem 1rem 1rem;
17+
padding: 2rem $sidebar-padding-right 1rem 1rem;
1718
border-right: 1px solid var(--pst-color-border);
1819
background-color: var(--pst-color-background);
1920
overflow-y: auto;
@@ -140,61 +141,107 @@
140141
.list-caption {
141142
list-style: none;
142143
padding-left: 0px;
143-
}
144-
li {
145-
position: relative;
146-
// If it has children, add a bit more padding to wrap the content to avoid
147-
// overlapping with the <label>
148-
&.has-children {
149-
> .reference {
150-
padding-right: 30px;
144+
145+
// Level 0 TOC heading is put inside the <summary> tag
146+
// so let the <summary> tag take up more space
147+
li.toctree-l0.has-children {
148+
> details {
149+
> summary {
150+
position: relative;
151+
height: auto;
152+
width: auto;
153+
display: flex;
154+
justify-content: space-between;
155+
align-items: baseline;
156+
157+
.toctree-toggle {
158+
// Prevent toggle icon from getting squished by summary being a
159+
// flexbox
160+
flex: 0 0 auto;
161+
162+
// Make the level 0 chevron icon slightly bigger than descendant
163+
// levels
164+
.fa-chevron-down {
165+
font-size: 1rem;
166+
}
167+
}
168+
}
151169
}
152170
}
153171
}
154-
// Navigation item chevrons
155-
label.toctree-toggle {
156-
position: absolute;
157-
top: 0;
158-
right: 0;
159-
height: 30px;
160-
width: 30px;
161-
162-
cursor: pointer;
172+
li.has-children {
173+
$toctree-toggle-width: 30px;
163174

164-
display: flex;
165-
justify-content: center;
166-
align-items: center;
175+
position: relative;
167176

168-
&:hover {
169-
background: var(--pst-color-surface);
177+
> .reference,
178+
.caption {
179+
margin-right: calc(
180+
$toctree-toggle-width + $focus-ring-width
181+
); // keep clear of the toggle icon
182+
padding-top: 0.25rem; // align caption text with toggle chevron
170183
}
171184

172-
i {
173-
display: inline-block;
174-
font-size: 0.75rem;
175-
text-align: center;
176-
&:hover {
177-
color: var(--pst-color-primary);
185+
> details {
186+
> summary {
187+
// Remove browser default toggle icon
188+
list-style: none;
189+
&::-webkit-details-marker {
190+
display: none;
191+
}
192+
193+
// The summary element is natively focusable, but delegate the focus state to the toggle icon
194+
&:focus-visible {
195+
outline: none;
196+
197+
> .toctree-toggle {
198+
outline: $focus-ring-outline;
199+
outline-offset: -$focus-ring-width; // Prevent right side of focus ring from disappearing underneath the sidebar's right edge
200+
}
201+
}
202+
203+
// Container for expand/collapse chevron icon
204+
.toctree-toggle {
205+
cursor: pointer;
206+
207+
// Position it so that it's aligned with the top right corner of the
208+
// last positioned element, in this case the li.has-children
209+
position: absolute;
210+
top: 0;
211+
right: 0;
212+
213+
// Give it dimensions
214+
width: $toctree-toggle-width;
215+
height: $toctree-toggle-width; // make it square
216+
217+
// Vertically and horizontally center the icon within the container
218+
display: inline-flex;
219+
justify-content: center;
220+
align-items: center;
221+
222+
.fa-chevron-down {
223+
font-size: 0.75rem;
224+
}
225+
}
226+
}
227+
228+
// The section is open/expanded, rotate the toggle icon (chevron) so it
229+
// points up instead of down
230+
&[open] {
231+
> summary {
232+
.fa-chevron-down {
233+
transform: rotate(180deg);
234+
}
235+
}
178236
}
179-
}
180-
}
181-
.label-parts {
182-
width: 100%;
183-
height: 100%;
184-
&:hover {
185-
background: none;
186-
}
187-
i {
188-
width: 30px;
189-
position: absolute;
190-
top: 0.3em; // aligning chevron with text
191-
right: 0em; // aligning chevron to the right
192237
}
193238
}
194239
}
195240

196241
/* Between-page links and captions */
197242
nav.bd-links {
243+
margin-right: -$sidebar-padding-right; // align toctree toggle chevrons with right edge of sidebar and allow text to flow closer to the right edge
244+
198245
@include media-breakpoint-up($breakpoint-sidebar-primary) {
199246
display: block;
200247
}
@@ -213,6 +260,7 @@ nav.bd-links {
213260
padding: 0.25rem 0.65rem;
214261
@include link-sidebar;
215262
box-shadow: none;
263+
margin-right: $focus-ring-width; // prevent the right side focus ring from disappearing under the sidebar right edge
216264

217265
&.reference.external {
218266
&:after {
@@ -224,11 +272,9 @@ nav.bd-links {
224272
}
225273
}
226274

227-
.current {
228-
> a {
229-
@include link-sidebar-current;
230-
background-color: transparent;
231-
}
275+
.current > a {
276+
@include link-sidebar-current;
277+
background-color: transparent;
232278
}
233279

234280
// Title

src/pydata_sphinx_theme/toctree.py

+75-33
Original file line numberDiff line numberDiff line change
@@ -339,10 +339,8 @@ def generate_toctree_html(
339339

340340
# Open the sidebar navigation to the proper depth
341341
for ii in range(int(show_nav_level)):
342-
for checkbox in soup.select(
343-
f"li.toctree-l{ii} > input.toctree-checkbox"
344-
):
345-
checkbox.attrs["checked"] = None
342+
for details in soup.select(f"li.toctree-l{ii} > details"):
343+
details["open"] = "open"
346344

347345
return soup
348346

@@ -422,8 +420,6 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None:
422420
"""Add checkboxes to collapse children in a toctree."""
423421
# based on https://github.com/pradyunsg/furo
424422

425-
toctree_checkbox_count = 0
426-
427423
for element in soup.find_all("li", recursive=True):
428424
# We check all "li" elements, to add a "current-page" to the correct li.
429425
classes = element.get("class", [])
@@ -432,7 +428,7 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None:
432428
if "current" in classes:
433429
parentli = element.find_parent("li", class_="toctree-l0")
434430
if parentli:
435-
parentli.select("p.caption ~ input")[0].attrs["checked"] = ""
431+
parentli.find("details")["open"] = None
436432

437433
# Nothing more to do, unless this has "children"
438434
if not element.find("ul"):
@@ -441,40 +437,86 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None:
441437
# Add a class to indicate that this has children.
442438
element["class"] = [*classes, "has-children"]
443439

444-
# We're gonna add a checkbox.
445-
toctree_checkbox_count += 1
446-
checkbox_name = f"toctree-checkbox-{toctree_checkbox_count}"
447-
448-
# Add the "label" for the checkbox which will get filled.
449440
if soup.new_tag is None:
450441
continue
451442

452-
label = soup.new_tag(
453-
"label", attrs={"for": checkbox_name, "class": "toctree-toggle"}
454-
)
455-
label.append(soup.new_tag("i", attrs={"class": "fa-solid fa-chevron-down"}))
456-
if "toctree-l0" in classes:
457-
# making label cover the whole caption text with css
458-
label["class"] = "label-parts"
459-
element.insert(1, label)
460-
461-
# Add the checkbox that's used to store expanded/collapsed state.
462-
checkbox = soup.new_tag(
463-
"input",
443+
# For table of contents nodes that have subtrees, we modify the HTML so
444+
# that the subtree can be expanded or collapsed in the browser.
445+
#
446+
# The HTML markup tree at the parent node starts with this structure:
447+
#
448+
# - li.has-children
449+
# - a.reference or p.caption
450+
# - ul
451+
#
452+
# Note the first child of li.has-children is p.caption only if this node
453+
# is a section heading. (This only happens when show_nav_level is set to
454+
# 0.)
455+
#
456+
# Now we modify the tree structure in one of two ways.
457+
#
458+
# (1) If the node holds a section heading, the HTML tree will be
459+
# modified like so:
460+
#
461+
# - li.has-children
462+
# - details
463+
# - summary
464+
# - p.caption
465+
# - .toctree-toggle
466+
# - ul
467+
#
468+
# (2) Otherwise, if the node holds a link to a page in the docs:
469+
#
470+
# - li.has-children
471+
# - a.reference
472+
# - details
473+
# - summary
474+
# - .toctree-toggle
475+
# - ul
476+
#
477+
# Why the difference? In the first case, the TOC section heading is not
478+
# a link, but in the second case it is. So in the first case it makes
479+
# sense to put the (non-link) text inside the summary tag so that the
480+
# user can click either the text or the .toctree-toggle chevron icon to
481+
# expand/collapse the TOC subtree. But in the second case, putting the
482+
# link in the summary tag would make it unclear whether clicking on the
483+
# link should expand the subtree or take you to the link.
484+
485+
# Create <details> and put the entire subtree into it
486+
details = soup.new_tag("details")
487+
details.extend(element.contents)
488+
element.append(details)
489+
490+
# Hoist the link to the top if there is one
491+
toc_link = element.select_one("details > a.reference")
492+
if toc_link:
493+
element.insert(0, toc_link)
494+
495+
# Create <summary> with chevron icon
496+
summary = soup.new_tag("summary")
497+
span = soup.new_tag(
498+
"span",
464499
attrs={
465-
"type": "checkbox",
466-
"class": ["toctree-checkbox"],
467-
"id": checkbox_name,
468-
"name": checkbox_name,
500+
"class": "toctree-toggle",
501+
"role": "presentation", # This element and the chevron it contains are purely decorative; the actual expand/collapse functionality is delegated to the <summary> tag
469502
},
470503
)
504+
span.append(soup.new_tag("i", attrs={"class": "fa-solid fa-chevron-down"}))
505+
summary.append(span)
471506

472-
# if this has a "current" class, be expanded by default
473-
# (by checking the checkbox)
474-
if "current" in classes:
475-
checkbox.attrs["checked"] = ""
507+
# Prepend section heading (if there is one) to <summary>
508+
collapsible_section_heading = element.select_one("details > p.caption")
509+
if collapsible_section_heading:
510+
# Put heading inside summary so that the heading text (and chevron) are both clickable
511+
summary.insert(0, collapsible_section_heading)
512+
513+
# Prepend <summary> to <details>
514+
details.insert(0, summary)
476515

477-
element.insert(1, checkbox)
516+
# If this TOC node has a "current" class, be expanded by default
517+
# (by opening the details/summary disclosure widget)
518+
if "current" in classes:
519+
details["open"] = "open"
478520

479521

480522
def get_nonroot_toctree(

0 commit comments

Comments
 (0)