diff --git a/src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss b/src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss index fb31f0c40..8270353f2 100644 --- a/src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss +++ b/src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss @@ -164,114 +164,59 @@ $link-hover-decoration-thickness: string.unquote( } } -// Navigation bar current page link styles -// --------------------------------------- -// Adds a bottom underline, this leaves enough space for the hover state without -// cluttering the navbar. -// We want the side box shadow to have the same thickness as the hover underline -@mixin link-navbar-current { - font-weight: 600; - color: var(--pst-color-primary); +// Heaver navbar text and icon links +// --------------------------------- +// (includes light/dark mode button) + +// This mixin makes it possible to show hover/underline and focus/ring styles at +// the same time. The trick is to use: +// - a pseudo-element with bottom border for the hover underline +// - a CSS outline for the focus ring. + +// Normally we use box-shadow for underline and outline for focus ring. But we +// cannot apply box-shadow and outline together on the same element because the +// border-radius value that we use to round the outline will also round the +// box-shadow used for the underline. We also cannot use text-underline because +// it does not work on non-text links, nor do we want to use it on text links +// that we want to treat as blocks, such as the header nav links because the +// underline will wrap across two lines if the link text also wraps across two +// lines. +@mixin link-style-block { + color: var(--pst-color-text-muted); - @if $link-hover-decoration-thickness { - border-bottom: $link-hover-decoration-thickness - solid - var(--pst-color-primary); - } -} + // Set position relative so that the child ::before pseudo-element's absolute + // position is relative to this element. + position: relative; -// Navigation bar icon links hover styles -// -------------------------------------- -// Adds a bottom box-shadow - since there is no text we cannot use text-decoration -// We want the side box shadow to have the same thickness as the hover underline -@mixin icon-navbar-hover { - &:hover { - color: var(--pst-color-link-hover); + // Set up pseudo-element used for hover underline styles + &::before { + content: ""; + display: block; + position: absolute; + inset: 0; + background-color: transparent; @if $link-hover-decoration-thickness { - box-shadow: 0 - $link-hover-decoration-thickness - 0 - var(--pst-color-link-hover); + bottom: calc(-1 * $link-hover-decoration-thickness); + margin: $link-hover-decoration-thickness 0; } } -} - -// 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); - } - - > .nav-link { - // Set up pseudo-element for hover and current states below. - position: relative; + &:hover { + color: var(--pst-color-secondary); + text-decoration: none; // override the link-style-hover mixin &::before { - content: ""; - display: block; - position: absolute; - inset: 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); + @if $link-hover-decoration-thickness { + border-bottom: $link-hover-decoration-thickness + solid + var(--pst-color-secondary); } } - - &:focus-visible { - box-shadow: none; // override Bootstrap - outline: 3px solid var(--pst-color-accent); - outline-offset: 3px; - } - } - - > .dropdown-toggle { - &:focus-visible { - box-shadow: $focus-ring-box-shadow; - } - - &: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); - } - } + &:focus-visible { + box-shadow: none; // override Bootstrap + outline: 3px solid var(--pst-color-accent); + outline-offset: 3px; } } diff --git a/src/pydata_sphinx_theme/assets/styles/abstracts/_mixins.scss b/src/pydata_sphinx_theme/assets/styles/abstracts/_mixins.scss index 29ef9d1ed..f72992a48 100644 --- a/src/pydata_sphinx_theme/assets/styles/abstracts/_mixins.scss +++ b/src/pydata_sphinx_theme/assets/styles/abstracts/_mixins.scss @@ -57,3 +57,14 @@ } } } + +// Minimum mouse hit area +// ---------------------- +// Ensures that the element has a minimum hit area that conforms to +// accessibility guidelines. For WCAG AA, we need 24px x 24px, see: +// https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html +@mixin min-hit-area() { + box-sizing: border-box; + min-width: 24px; + min-height: 24px; +} diff --git a/src/pydata_sphinx_theme/assets/styles/base/_base.scss b/src/pydata_sphinx_theme/assets/styles/base/_base.scss index 5748472c7..4fb0b0600 100644 --- a/src/pydata_sphinx_theme/assets/styles/base/_base.scss +++ b/src/pydata_sphinx_theme/assets/styles/base/_base.scss @@ -7,7 +7,7 @@ body { background-color: var(--pst-color-background); font-family: var(--pst-font-family-base); font-weight: 400; - line-height: 1.65; + line-height: $line-height-body; color: var(--pst-color-text-base); min-height: 100vh; display: flex; diff --git a/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss b/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss index ab0748c06..8edba3fdb 100644 --- a/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss +++ b/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss @@ -2,7 +2,29 @@ * Icon links in the navbar */ -.navbar-icon-links { +.pst-navbar-icon { + // Extra specificity needed for overrides + html & { + @include min-hit-area; + @include link-style-block; + + display: flex; + align-items: center; + justify-content: center; + + // Bootstrap overrides + border-radius: 0; + border: none; + font-size: 1rem; + line-height: $line-height-body; // Override Boostrap, which defines a separate line-height for buttons + padding: $navbar-link-padding-y 0; // Horizontal white space in nav bar between items is controlled via column gap rule on the container. + + // Make the navbar icon links have the same size as the navbar text links + height: calc(2 * $navbar-link-padding-y + $line-height-body * 1rem); + } +} + +ul.navbar-icon-links { display: flex; flex-flow: row wrap; column-gap: 1rem; @@ -12,24 +34,6 @@ margin-bottom: 0; list-style: none; - // Remove the padding so that we can define it with flexbox gap above - li.nav-item a.nav-link { - padding-left: 0; - padding-right: 0; - - @include icon-navbar-hover; - - &:focus { - color: inherit; - } - } - - // Spacing and centering - a span { - display: flex; - align-items: center; - } - // Icons styling i { &.fa-brands, diff --git a/src/pydata_sphinx_theme/assets/styles/components/_navbar-links.scss b/src/pydata_sphinx_theme/assets/styles/components/_navbar-links.scss index 243722e4b..ea853971a 100644 --- a/src/pydata_sphinx_theme/assets/styles/components/_navbar-links.scss +++ b/src/pydata_sphinx_theme/assets/styles/components/_navbar-links.scss @@ -1,16 +1,10 @@ /** * Navigation links in the navbar and icon links */ -.navbar-nav, -.navbar-icon-links { +ul.navbar-nav { + // Reduce padding of nested `ul` items a bit ul { - display: block; - list-style: none; - - // Reduce padding of nested `ul` items a bit - ul { - padding: 0 0 0 1rem; - } + padding: 0 0 0 1rem; } // Navbar links - do not have an underline by default @@ -22,8 +16,8 @@ display: flex; align-items: center; height: 100%; - padding-top: 0.25rem; - padding-bottom: 0.25rem; + padding-top: $navbar-link-padding-y; + padding-bottom: $navbar-link-padding-y; @include link-style-text; } diff --git a/src/pydata_sphinx_theme/assets/styles/components/_search.scss b/src/pydata_sphinx_theme/assets/styles/components/_search.scss index f0144f043..ce8256d4c 100644 --- a/src/pydata_sphinx_theme/assets/styles/components/_search.scss +++ b/src/pydata_sphinx_theme/assets/styles/components/_search.scss @@ -67,26 +67,8 @@ */ // Search link icon should be a bit bigger since it is separate from icon links -.search-button { - display: flex; - align-items: center; - align-content: center; - color: var(--pst-color-text-muted); - padding: 0; - border-radius: 0; - border: none; // Override Bootstrap button border - font-size: 1rem; // Override Bootstrap button font size - - // Override Bootstrap button padding-x. Whitespace in nav bar is controlled - // via column gap rule on the container. - padding-left: 0; - padding-right: 0; - - @include icon-navbar-hover; - - i { - font-size: 1.3rem; - } +.search-button i { + font-size: 1.3rem; } // __search-container will only show up when we use the search pop-up bar diff --git a/src/pydata_sphinx_theme/assets/styles/components/_switcher-theme.scss b/src/pydata_sphinx_theme/assets/styles/components/_switcher-theme.scss index 19b180124..8fe5d5197 100644 --- a/src/pydata_sphinx_theme/assets/styles/components/_switcher-theme.scss +++ b/src/pydata_sphinx_theme/assets/styles/components/_switcher-theme.scss @@ -3,21 +3,7 @@ */ .theme-switch-button { - color: var(--pst-color-text-muted); - border-radius: 0; - border: none; // Override Bootstrap button border - font-size: 1rem; // Override Bootstrap's button font size - - // Override Bootstrap button padding-x. Whitespace in nav bar is controlled - // via column gap rule on the container. - padding-left: 0; - padding-right: 0; - - &:hover { - @include icon-navbar-hover; - } - - span { + .theme-switch { display: none; &:active { @@ -31,14 +17,10 @@ } } -html[data-mode="auto"] .theme-switch-button span[data-mode="auto"] { - display: flex; -} - -html[data-mode="light"] .theme-switch-button span[data-mode="light"] { - display: flex; -} - -html[data-mode="dark"] .theme-switch-button span[data-mode="dark"] { - display: flex; +@each $mode in auto, light, dark { + html[data-mode="#{$mode}"] + .theme-switch-button + .theme-switch[data-mode="#{$mode}"] { + display: inline; // inline needed for span height to be calculated using inherited font size and line height + } } diff --git a/src/pydata_sphinx_theme/assets/styles/components/_switcher-version.scss b/src/pydata_sphinx_theme/assets/styles/components/_switcher-version.scss index 01ae283cc..a5dd61403 100644 --- a/src/pydata_sphinx_theme/assets/styles/components/_switcher-version.scss +++ b/src/pydata_sphinx_theme/assets/styles/components/_switcher-version.scss @@ -72,6 +72,10 @@ button.version-switcher__button, font-size: 1.1em; // A bit smaller than other menu font z-index: $zindex-modal; // higher than the sidebars + // Make sure it meets WCAG target size requirement no matter the version + // string displayed in the button + @include min-hit-area; + @include media-breakpoint-up($breakpoint-sidebar-primary) { font-size: unset; } diff --git a/src/pydata_sphinx_theme/assets/styles/sections/_header.scss b/src/pydata_sphinx_theme/assets/styles/sections/_header.scss index 34ac624e7..b2c969ab1 100644 --- a/src/pydata_sphinx_theme/assets/styles/sections/_header.scss +++ b/src/pydata_sphinx_theme/assets/styles/sections/_header.scss @@ -81,7 +81,7 @@ } // Contains the navigation links within the navbar - .navbar-nav { + ul.navbar-nav { display: flex; @include media-breakpoint-up($breakpoint-sidebar-primary) { @@ -89,21 +89,49 @@ align-items: baseline; } - li.pst-header-nav-item { + > li.nav-item { margin-inline: 2px; // breathing room so hover and focus styles do not overlap + + > .nav-link { + @include link-style-block; + + padding-inline: 6px; + } + + &.current { + > .nav-link { + color: var(--pst-color-primary); + + // Underline the current navbar item + &::before { + border-bottom: 3px solid var(--pst-color-primary); + } + } + } + &.dropdown { margin-inline: 4px; button { padding-inline: 8px; } - } - a { - padding-inline: 6px; - } + > .dropdown-toggle { + border-radius: $focus-ring-radius; // make border radius the same for both hover ring and focus ring + color: var(--pst-color-text-muted); + + &:focus-visible { + box-shadow: $focus-ring-box-shadow; + } - @include header-link; + &: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); + } + } + } } li a.nav-link.dropdown-item { @@ -159,17 +187,11 @@ // Toggle buttons button.sidebar-toggle { - display: flex; - cursor: pointer; font-size: var(--pst-font-size-icon); - align-items: center; color: var(--pst-color-muted); margin-bottom: 0; - padding-bottom: 0.25rem; background-color: inherit; - border: none; - - @include icon-navbar-hover; + padding: 0.5rem; } button.primary-toggle { diff --git a/src/pydata_sphinx_theme/assets/styles/sections/_sidebar-primary.scss b/src/pydata_sphinx_theme/assets/styles/sections/_sidebar-primary.scss index 518b05b71..7bf74c387 100644 --- a/src/pydata_sphinx_theme/assets/styles/sections/_sidebar-primary.scss +++ b/src/pydata_sphinx_theme/assets/styles/sections/_sidebar-primary.scss @@ -254,6 +254,7 @@ nav.bd-links { } ul { + display: block; list-style: none; // Reduce padding of nested `ul` items a bit diff --git a/src/pydata_sphinx_theme/assets/styles/variables/_fonts.scss b/src/pydata_sphinx_theme/assets/styles/variables/_fonts.scss index 97ef52f92..19219c3d4 100644 --- a/src/pydata_sphinx_theme/assets/styles/variables/_fonts.scss +++ b/src/pydata_sphinx_theme/assets/styles/variables/_fonts.scss @@ -41,3 +41,5 @@ html { --pst-font-family-heading: var(--pst-font-family-base-system); --pst-font-family-monospace: var(--pst-font-family-monospace-system); } + +$line-height-body: 1.65; diff --git a/src/pydata_sphinx_theme/assets/styles/variables/_layout.scss b/src/pydata_sphinx_theme/assets/styles/variables/_layout.scss index 1fbd63d8b..eaae298a2 100644 --- a/src/pydata_sphinx_theme/assets/styles/variables/_layout.scss +++ b/src/pydata_sphinx_theme/assets/styles/variables/_layout.scss @@ -35,3 +35,5 @@ $admonition-border-radius: 0.25rem; // In this theme, some focus rings have rounded corners while others do not. // This variable sets the border radius for the rounded focus rings. $focus-ring-radius: 0.125rem; // 2px at 100% zoom and 16px base font. + +$navbar-link-padding-y: 0.25rem; diff --git a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/icon-links.html b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/icon-links.html index c40e2638a..50a7dcec9 100644 --- a/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/icon-links.html +++ b/src/pydata_sphinx_theme/theme/pydata_sphinx_theme/components/icon-links.html @@ -2,7 +2,7 @@ {%- macro icon_link_nav_item(url, icon, name, type, attributes='') -%} {%- if url | length > 2 %}
  • + {title}
  • """ + nav_item = "nav-item" + nav_link = "nav-link" for link in links_data: links_html.append( dedent( boilerplate.format( active=" current active" if link.is_current else "", + nav_link=nav_link, + nav_item=nav_item, ext_int="external" if link.is_external else "internal", href=link.href, title=link.title, @@ -244,12 +248,7 @@ def _generate_header_nav_before_dropdown( # Wrap the final few header items in a "more" dropdown links_dropdown = [ - # 🐲 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", - "", - ) + html.replace(nav_item, "").replace(nav_link, "nav-link dropdown-item") for html in links_html[n_links_before_dropdown:] ] @@ -284,7 +283,7 @@ def generate_header_nav_html( dropdown_id = unique_html_id("pst-nav-more-links") links_dropdown_html = "\n".join(links_dropdown) out += f""" -