Skip to content

Header nav link styles (focus, hover, current) #1545

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

Merged
merged 15 commits into from
Dec 13, 2023
Merged
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
16 changes: 16 additions & 0 deletions docs/_static/pydata-icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*******************************************************************************
* Set a custom icon for PyData
*/
FontAwesome.library.add(
(faListOldStyle = {
prefix: "fa-custom",
iconName: "pydata",
icon: [
24, // viewBox width
24, // viewBox height
[], // ligature
"e002", // unicode codepoint - private use area
"M12.1,17.8v5.8l-5-2.9v-5.8L12.1,17.8z M12.1,12v5.8l-5-2.9V9.1L12.1,12z M17,9.1L12.1,12v5.8l4.9-2.9V9.1z M12.1,6.2L7,9.1l5,2.9L17,9.1L12.1,6.2z M17,9.1V3.3l-4.9-2.8v5.8L17,9.1z",
],
})
);
Binary file removed docs/_static/pydata-logo.png
Binary file not shown.
6 changes: 2 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@
{
"name": "PyData",
"url": "https://pydata.org",
"icon": "_static/pydata-logo.png",
"type": "local",
"attributes": {"target": "_blank"},
"icon": "fa-custom fa-pydata",
},
],
# alternative way to set twitter and github header icons
Expand Down Expand Up @@ -226,7 +224,7 @@
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_css_files = ["custom.css"]
html_js_files = ["custom-icon.js"]
html_js_files = ["pydata-icon.js", "custom-icon.js"]
todo_include_todos = True

# -- favicon options ---------------------------------------------------------
Expand Down
80 changes: 80 additions & 0 deletions src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,83 @@ $link-hover-decoration-thickness: unquote("max(3px, .1875rem, .12em)") !default;
}
}
}

/*
Mixin for links in the header (and the More dropdown toggle).

The mixin assumes it will be applied to some element X with a markup structure
like: X > .nav-link, or X > .dropdown-toggle.

It also assumes X.current is how the app annotates which item in the header nav
corresponds to the section in the docs that the user is currently reading.
*/
@mixin header-link {
// Target the child and not the parent because we want the underline in the
// mobile sidebar to only span the width of the text not the entire row/line.
> .nav-link,
> .dropdown-toggle {
border-radius: 2px;
color: var(--pst-color-text-muted);
font-weight: 700;

&:focus-visible {
box-shadow: none; // override Bootstrap
outline: 3px solid var(--pst-color-accent);
outline-offset: 3px;
}
}

> .nav-link {
// Set up pseudo-element for hover and current states below.
position: relative;
&::before {
content: "";
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: transparent;
}

// Underline on hover.
// - Don't use text-decoration because it will wrap across two lines if
// the link text also wraps across two lines.
// - Use pseudo-element in order to avoid the border-radius values
// rounding the edges of the underline. (And since a header link can be
// both focused and hovered at the same time and we want the focus ring
// but not the underline to be rounded, we cannot use a box shadow or
// bottom border link element to create the underline, or else it will
// be rounded and if we apply border-radius 0 then the hovered focus
// ring would go from rounded to sharp. So we have to use the
// pseudo-element.)
&:hover {
color: var(--pst-color-secondary);
text-decoration: none; // override the link-style-hover mixin
&::before {
border-bottom: 3px solid var(--pst-color-secondary);
}
}
}

> .dropdown-toggle {
&:hover {
text-decoration: none;
box-shadow: 0 0 0 $focus-ring-width var(--pst-color-link-hover); // purple focus ring
// Brighten the text on hover (muted -> base)
color: var(--pst-color-text-base);
}
}

&.current {
> .nav-link {
color: var(--pst-color-primary);

// Underline the current navbar item
&::before {
border-bottom: 3px solid var(--pst-color-primary);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
padding-left: 0;
padding-right: 0;
@include icon-navbar-hover;
&:focus {
color: inherit;
}
}

// Spacing and centering
Expand Down Expand Up @@ -50,4 +53,10 @@
height: 1.5em;
border-radius: 0.2rem;
}

.fa-pydata {
stroke: var(--pst-color-background);
stroke-linejoin: round;
stroke-width: 0.35;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
background-color: var(--pst-color-surface);

&:hover {
border: 2px solid var(--pst-color-link-hover);
box-shadow: 0 0 0 $focus-ring-width var(--pst-color-link-hover);
}

&:focus-visible {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ button.btn.version-switcher__button {
@include media-breakpoint-up($breakpoint-sidebar-primary) {
margin-bottom: unset;
}

@include link-style-hover;
&:hover {
box-shadow: 0 0 0 $focus-ring-width var(--pst-color-secondary);
border-color: transparent;
}
&:active {
color: var(--pst-color-text-base);
border-color: var(--pst-color-border);
}
&:focus-visible {
border-color: transparent;
}
}

.version-switcher__menu {
Expand Down
19 changes: 12 additions & 7 deletions src/pydata_sphinx_theme/assets/styles/sections/_header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,23 +84,28 @@

@include media-breakpoint-up($breakpoint-sidebar-primary) {
// Center align on wide screens so the dropdown button is centered properly
align-items: center;
align-items: baseline;
}

li a.nav-link {
@include link-style-text;
li.pst-header-nav-item {
margin-inline: 5px; // breathing room so hover, focus styles do not overlap
&:first-child {
margin-inline-start: 0;
}
&:last-child {
margin-inline-end: 0;
}
@include header-link;
}

// Current page is always underlined in the navbar
> .current > .nav-link {
@include link-navbar-current;
li a.nav-link.dropdown-item {
@include link-style-text;
}

// Dropdowns for the extra links
.dropdown {
button {
display: unset;
color: var(--pst-color-text-muted);
border: none;
@include link-style-hover;
}
Expand Down
21 changes: 12 additions & 9 deletions src/pydata_sphinx_theme/toctree.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
page = toc.attributes["parent"] if page == "self" else page

# If this is the active ancestor page, add a class so we highlight it
current = " current active" if page == active_header_page else ""
current = "current active" if page == active_header_page else ""

# sanitize page title for use in the html output if needed
if title is None:
Expand All @@ -108,14 +108,14 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
# If it's an absolute one then we use the external class and
# the complete url.
is_absolute = bool(urlparse(page).netloc)
link_status = "external" if is_absolute else "internal"
link_status = "nav-external" if is_absolute else "nav-internal"
link_href = page if is_absolute else context["pathto"](page)

# create the html output
links_html.append(
f"""
<li class="nav-item{current}">
<a class="nav-link nav-{link_status}" href="{link_href}">
<li class="nav-item pst-header-nav-item {current}">
<a class="nav-link {link_status}" href="{link_href}">
{title}
</a>
</li>
Expand All @@ -126,7 +126,7 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):
for external_link in context["theme_external_links"]:
links_html.append(
f"""
<li class="nav-item">
<li class="nav-item pst-header-nav-item">
<a class="nav-link nav-external" href="{ external_link["url"] }">
{ external_link["name"] }
</a>
Expand All @@ -140,9 +140,12 @@ def generate_header_nav_before_dropdown(n_links_before_dropdown):

# Wrap the final few header items in a "more" dropdown
links_dropdown = [
# 🐲 brittle code, relies on the assumption that the code above
# gives each link in the nav a `nav-link` CSS class
html.replace("nav-link", "nav-link dropdown-item")
# 🐲 brittle code because it relies on the code above to build the HTML in a particular way
html.replace("nav-link", "nav-link dropdown-item").replace(
# Prevents the header-link mixin from applying to links within the dropdown
"pst-header-nav-item",
"",
)
for html in links_html[n_links_before_dropdown:]
]

Expand Down Expand Up @@ -177,7 +180,7 @@ def generate_header_nav_html(
dropdown_id = unique_html_id("pst-nav-more-links")
links_dropdown_html = "\n".join(links_dropdown)
out += f"""
<li class="nav-item dropdown">
<li class="nav-item dropdown pst-header-nav-item">
<button class="btn dropdown-toggle nav-item" type="button" data-bs-toggle="dropdown" aria-expanded="false" aria-controls="{dropdown_id}">
{_(dropdown_text)}
</button>
Expand Down