-
Notifications
You must be signed in to change notification settings - Fork 339
Make TOC sections expandable and collapsible by keyboard #1582
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
Changes from all commits
85badcb
920c91d
38ef477
9803c5e
ba86357
2c35dac
598ebed
7b43a8d
eda8049
a009423
741af85
b57b376
967f598
cb6207d
01e5f23
791ff49
c76033d
1ba63b3
dc567ca
4ff5404
5f5faa5
85f2ad1
312f3b8
309c6b0
123140c
89eba83
0d8693e
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 |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
* e.g., between-pages navigation. | ||
*/ | ||
|
||
$sidebar-padding-right: 1rem; | ||
.bd-sidebar-primary { | ||
display: flex; | ||
flex-direction: column; | ||
|
@@ -13,7 +14,7 @@ | |
@include make-col(3); | ||
|
||
// Borders padding and whitespace | ||
padding: 2rem 1rem 1rem 1rem; | ||
padding: 2rem $sidebar-padding-right 1rem 1rem; | ||
border-right: 1px solid var(--pst-color-border); | ||
background-color: var(--pst-color-background); | ||
overflow-y: auto; | ||
|
@@ -140,61 +141,107 @@ | |
.list-caption { | ||
list-style: none; | ||
padding-left: 0px; | ||
} | ||
li { | ||
position: relative; | ||
// If it has children, add a bit more padding to wrap the content to avoid | ||
// overlapping with the <label> | ||
&.has-children { | ||
> .reference { | ||
padding-right: 30px; | ||
|
||
// Level 0 TOC heading is put inside the <summary> tag | ||
// so let the <summary> tag take up more space | ||
li.toctree-l0.has-children { | ||
> details { | ||
> summary { | ||
position: relative; | ||
height: auto; | ||
width: auto; | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: baseline; | ||
|
||
.toctree-toggle { | ||
// Prevent toggle icon from getting squished by summary being a | ||
// flexbox | ||
Comment on lines
+158
to
+159
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. just want to say I really appreciate comments like this in the CSS. Makes reviewing so much easier when the intent is clear. |
||
flex: 0 0 auto; | ||
|
||
// Make the level 0 chevron icon slightly bigger than descendant | ||
// levels | ||
.fa-chevron-down { | ||
font-size: 1rem; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// Navigation item chevrons | ||
label.toctree-toggle { | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
height: 30px; | ||
width: 30px; | ||
|
||
cursor: pointer; | ||
li.has-children { | ||
$toctree-toggle-width: 30px; | ||
|
||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
position: relative; | ||
|
||
&:hover { | ||
background: var(--pst-color-surface); | ||
> .reference, | ||
.caption { | ||
margin-right: calc( | ||
$toctree-toggle-width + $focus-ring-width | ||
); // keep clear of the toggle icon | ||
padding-top: 0.25rem; // align caption text with toggle chevron | ||
} | ||
|
||
i { | ||
display: inline-block; | ||
font-size: 0.75rem; | ||
text-align: center; | ||
&:hover { | ||
color: var(--pst-color-primary); | ||
> details { | ||
> summary { | ||
// Remove browser default toggle icon | ||
list-style: none; | ||
&::-webkit-details-marker { | ||
display: none; | ||
} | ||
|
||
// The summary element is natively focusable, but delegate the focus state to the toggle icon | ||
&:focus-visible { | ||
outline: none; | ||
|
||
> .toctree-toggle { | ||
outline: $focus-ring-outline; | ||
outline-offset: -$focus-ring-width; // Prevent right side of focus ring from disappearing underneath the sidebar's right edge | ||
} | ||
} | ||
|
||
// Container for expand/collapse chevron icon | ||
.toctree-toggle { | ||
cursor: pointer; | ||
|
||
// Position it so that it's aligned with the top right corner of the | ||
// last positioned element, in this case the li.has-children | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
|
||
// Give it dimensions | ||
width: $toctree-toggle-width; | ||
height: $toctree-toggle-width; // make it square | ||
|
||
// Vertically and horizontally center the icon within the container | ||
display: inline-flex; | ||
justify-content: center; | ||
align-items: center; | ||
|
||
.fa-chevron-down { | ||
font-size: 0.75rem; | ||
} | ||
} | ||
} | ||
|
||
// The section is open/expanded, rotate the toggle icon (chevron) so it | ||
// points up instead of down | ||
&[open] { | ||
> summary { | ||
.fa-chevron-down { | ||
transform: rotate(180deg); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
.label-parts { | ||
width: 100%; | ||
height: 100%; | ||
&:hover { | ||
background: none; | ||
} | ||
i { | ||
width: 30px; | ||
position: absolute; | ||
top: 0.3em; // aligning chevron with text | ||
right: 0em; // aligning chevron to the right | ||
} | ||
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. The other parts of this PR suggest that this class was applied to the |
||
} | ||
} | ||
|
||
/* Between-page links and captions */ | ||
nav.bd-links { | ||
margin-right: -$sidebar-padding-right; // align toctree toggle chevrons with right edge of sidebar and allow text to flow closer to the right edge | ||
|
||
@include media-breakpoint-up($breakpoint-sidebar-primary) { | ||
display: block; | ||
} | ||
|
@@ -213,6 +260,7 @@ nav.bd-links { | |
padding: 0.25rem 0.65rem; | ||
@include link-sidebar; | ||
box-shadow: none; | ||
margin-right: $focus-ring-width; // prevent the right side focus ring from disappearing under the sidebar right edge | ||
|
||
&.reference.external { | ||
&:after { | ||
|
@@ -224,11 +272,9 @@ nav.bd-links { | |
} | ||
} | ||
|
||
.current { | ||
> a { | ||
@include link-sidebar-current; | ||
background-color: transparent; | ||
} | ||
.current > a { | ||
@include link-sidebar-current; | ||
background-color: transparent; | ||
} | ||
|
||
// Title | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -272,10 +272,8 @@ def generate_toctree_html( | |
|
||
# Open the sidebar navigation to the proper depth | ||
for ii in range(int(show_nav_level)): | ||
for checkbox in soup.select( | ||
f"li.toctree-l{ii} > input.toctree-checkbox" | ||
): | ||
checkbox.attrs["checked"] = None | ||
for details in soup.select(f"li.toctree-l{ii} > details"): | ||
details["open"] = "open" | ||
|
||
return soup | ||
|
||
|
@@ -354,8 +352,6 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None: | |
"""Add checkboxes to collapse children in a toctree.""" | ||
# based on https://github.com/pradyunsg/furo | ||
|
||
toctree_checkbox_count = 0 | ||
|
||
for element in soup.find_all("li", recursive=True): | ||
# We check all "li" elements, to add a "current-page" to the correct li. | ||
classes = element.get("class", []) | ||
|
@@ -364,7 +360,7 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None: | |
if "current" in classes: | ||
parentli = element.find_parent("li", class_="toctree-l0") | ||
if parentli: | ||
parentli.select("p.caption ~ input")[0].attrs["checked"] = "" | ||
parentli.find("details")["open"] = None | ||
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. nice side effect of using |
||
|
||
# Nothing more to do, unless this has "children" | ||
if not element.find("ul"): | ||
|
@@ -373,40 +369,86 @@ def add_collapse_checkboxes(soup: BeautifulSoup) -> None: | |
# Add a class to indicate that this has children. | ||
element["class"] = classes + ["has-children"] | ||
|
||
# We're gonna add a checkbox. | ||
toctree_checkbox_count += 1 | ||
checkbox_name = f"toctree-checkbox-{toctree_checkbox_count}" | ||
|
||
# Add the "label" for the checkbox which will get filled. | ||
if soup.new_tag is None: | ||
continue | ||
|
||
label = soup.new_tag( | ||
"label", attrs={"for": checkbox_name, "class": "toctree-toggle"} | ||
) | ||
label.append(soup.new_tag("i", attrs={"class": "fa-solid fa-chevron-down"})) | ||
if "toctree-l0" in classes: | ||
# making label cover the whole caption text with css | ||
label["class"] = "label-parts" | ||
element.insert(1, label) | ||
|
||
# Add the checkbox that's used to store expanded/collapsed state. | ||
checkbox = soup.new_tag( | ||
"input", | ||
# For table of contents nodes that have subtrees, we modify the HTML so | ||
# that the subtree can be expanded or collapsed in the browser. | ||
# | ||
# The HTML markup tree at the parent node starts with this structure: | ||
# | ||
# - li.has-children | ||
# - a.reference or p.caption | ||
# - ul | ||
# | ||
# Note the first child of li.has-children is p.caption only if this node | ||
# is a section heading. (This only happens when show_nav_level is set to | ||
# 0.) | ||
# | ||
# Now we modify the tree structure in one of two ways. | ||
# | ||
# (1) If the node holds a section heading, the HTML tree will be | ||
# modified like so: | ||
# | ||
# - li.has-children | ||
# - details | ||
# - summary | ||
# - p.caption | ||
# - .toctree-toggle | ||
# - ul | ||
# | ||
# (2) Otherwise, if the node holds a link to a page in the docs: | ||
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 am struggling to grapple some of the specifics so apologies if this does not make sense at all. Why does it only matter for case 1) that 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. Good question. This is fairly complicated to explain. Let's begin by reviewing the levels of navigation on the site:
The first level is put in the top nav, all the other levels are put in the left sidebar, except the last level, the within (sub)page level, which is put in the right sidebar. Normally, when the table of contents is generated for the left sidebar, several nested tree structures using HTML lists of nested lists are created to capture each group of pages and sub-pages within a part, but the parts themselves are rendered outside of these nested list structures. However, when Note that this part of the code you've asked about is in a function called But here's the catch. Unlike the pages and sub-pages (nav level > 0), the section parts (nav level = 0) are not links; there is no URL and no page in the site that maps to any section part. This is the key issue. Every node in the sidebar table of contents is a link except for the section headings, which are all 0th-level nodes in the sidebar (or 1st-level nodes if you consider the "Section Navigation" heading at the top to be the root node for the entire sidebar TOC). To reiterate, the key difference is that section nodes are not links, whereas page and sub-page nodes are. When a node is a link, we have to think about whether we want to put the link inside the summary tag or not. Why? If we put a link in a summary tag, we introduce an ambiguity: is the link click to expand/collapse or to navigate to a new page? I made the decision that the link click should be a link click and not an expand/collapse. However, I saw no reason why the user shouldn't be able to click the text of the section part to expand/collapse the details/summary, since a section part is not a link. When 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 just updated the code comment here. Hopefully, it's slightly clearer, or at least more precise, now. 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. Thanks for the in-depth explanation; this makes sense. I went through the rest of the PR again and could not spot any obvious gotchas. So, I am happy to approve it as is. That being said I know @drammock is a lot more familiar with the whole parsing/HTML generation side of things so I want to give him the chance to go over this or I can merge in a couple of days. |
||
# | ||
# - li.has-children | ||
# - a.reference | ||
# - details | ||
# - summary | ||
# - .toctree-toggle | ||
# - ul | ||
# | ||
# Why the difference? In the first case, the TOC section heading is not | ||
# a link, but in the second case it is. So in the first case it makes | ||
# sense to put the (non-link) text inside the summary tag so that the | ||
# user can click either the text or the .toctree-toggle chevron icon to | ||
# expand/collapse the TOC subtree. But in the second case, putting the | ||
# link in the summary tag would make it unclear whether clicking on the | ||
# link should expand the subtree or take you to the link. | ||
|
||
# Create <details> and put the entire subtree into it | ||
details = soup.new_tag("details") | ||
details.extend(element.contents) | ||
element.append(details) | ||
|
||
# Hoist the link to the top if there is one | ||
toc_link = element.select_one("details > a.reference") | ||
if toc_link: | ||
element.insert(0, toc_link) | ||
|
||
# Create <summary> with chevron icon | ||
summary = soup.new_tag("summary") | ||
span = soup.new_tag( | ||
"span", | ||
attrs={ | ||
"type": "checkbox", | ||
"class": ["toctree-checkbox"], | ||
"id": checkbox_name, | ||
"name": checkbox_name, | ||
"class": "toctree-toggle", | ||
"role": "presentation", # This element and the chevron it contains are purely decorative; the actual expand/collapse functionality is delegated to the <summary> tag | ||
}, | ||
) | ||
span.append(soup.new_tag("i", attrs={"class": "fa-solid fa-chevron-down"})) | ||
summary.append(span) | ||
|
||
# if this has a "current" class, be expanded by default | ||
# (by checking the checkbox) | ||
if "current" in classes: | ||
checkbox.attrs["checked"] = "" | ||
# Prepend section heading (if there is one) to <summary> | ||
collapsible_section_heading = element.select_one("details > p.caption") | ||
if collapsible_section_heading: | ||
# Put heading inside summary so that the heading text (and chevron) are both clickable | ||
summary.insert(0, collapsible_section_heading) | ||
|
||
# Prepend <summary> to <details> | ||
details.insert(0, summary) | ||
|
||
element.insert(1, checkbox) | ||
# If this TOC node has a "current" class, be expanded by default | ||
# (by opening the details/summary disclosure widget) | ||
if "current" in classes: | ||
details["open"] = "open" | ||
|
||
|
||
def get_local_toctree_for( | ||
|
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.
No more of the label/checkbox implementation (replaced in this PR with details/summary)