From 40695782e6322f57c738981c907537cfedcf97a3 Mon Sep 17 00:00:00 2001 From: gabalafou Date: Thu, 30 May 2024 19:53:48 +0200 Subject: [PATCH 1/6] Ensure hit area for buttons and icon links in nav bar meet WCAG --- .../assets/styles/abstracts/_links.scss | 155 +++++++----------- .../assets/styles/abstracts/_mixins.scss | 11 ++ .../assets/styles/components/_icon-links.scss | 43 ++--- .../styles/components/_navbar-links.scss | 12 +- .../assets/styles/components/_search.scss | 22 +-- .../styles/components/_switcher-theme.scss | 20 +-- .../styles/components/_switcher-version.scss | 4 + .../assets/styles/sections/_header.scss | 44 ++++- .../components/icon-links.html | 6 +- .../components/search-button-field.html | 2 +- .../components/search-button.html | 4 +- .../components/theme-switcher.html | 2 +- .../components/version-switcher.html | 2 +- src/pydata_sphinx_theme/toctree.py | 17 +- 14 files changed, 155 insertions(+), 189 deletions(-) diff --git a/src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss b/src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss index fb31f0c40..0c63fa6f7 100644 --- a/src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss +++ b/src/pydata_sphinx_theme/assets/styles/abstracts/_links.scss @@ -61,6 +61,21 @@ $link-hover-decoration-thickness: string.unquote( color: var(--pst-color-link-hover); } +// Hover underline for icons / non-text content +// - 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. +// - Use with &:hover. +@mixin icon-hover { + color: var(--pst-color-link-hover); + + @if $link-hover-decoration-thickness { + box-shadow: 0 + $link-hover-decoration-thickness + 0 + var(--pst-color-link-hover); + } +} + // Default link styles // ------------------- // Defines: default unvisited, visited, hover, and active. @@ -164,114 +179,54 @@ $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); - - @if $link-hover-decoration-thickness { - border-bottom: $link-hover-decoration-thickness - solid - var(--pst-color-primary); - } -} - -// 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); - - @if $link-hover-decoration-thickness { - box-shadow: 0 - $link-hover-decoration-thickness - 0 - var(--pst-color-link-hover); - } - } -} +// 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); -// Mixin for links in the header (and the More dropdown toggle). + // Set position relative so that the child ::before pseudo-element's absolute + // position is relative to this element. + position: relative; -// The mixin assumes it will be applied to some element X with a markup structure -// like: X > .nav-link, or X > .dropdown-toggle. + // Put some space between the underline and the content. Must apply to top as + // well as bottom so that the link is centered within the focus ring. + padding-block: 0.25rem; -// 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); + // Pseudo-element for hover underline styles + &::before { + content: ""; + display: block; + position: absolute; + inset: 0; + background-color: transparent; } - > .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); - } - } - - &:focus-visible { - box-shadow: none; // override Bootstrap - outline: 3px solid var(--pst-color-accent); - outline-offset: 3px; + border-bottom: 3px solid var(--pst-color-secondary); } } - > .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/components/_icon-links.scss b/src/pydata_sphinx_theme/assets/styles/components/_icon-links.scss index ab0748c06..64a86d4e6 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,30 @@ * 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: inherit; // Bootstrap defines a separate line-height for buttons + + // Override Bootstrap button padding-x. Whitespace in nav bar is controlled + // via column gap rule on the container. + padding-left: 0; + padding-right: 0; + } +} + +ul.navbar-icon-links { display: flex; flex-flow: row wrap; column-gap: 1rem; @@ -12,24 +35,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..38e26afb1 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 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..76a993d41 100644 --- a/src/pydata_sphinx_theme/assets/styles/components/_switcher-theme.scss +++ b/src/pydata_sphinx_theme/assets/styles/components/_switcher-theme.scss @@ -3,20 +3,6 @@ */ .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 { display: none; @@ -32,13 +18,13 @@ } html[data-mode="auto"] .theme-switch-button span[data-mode="auto"] { - display: flex; + display: inline; } html[data-mode="light"] .theme-switch-button span[data-mode="light"] { - display: flex; + display: inline; } html[data-mode="dark"] .theme-switch-button span[data-mode="dark"] { - display: flex; + display: inline; } 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..4325262b0 100644 --- a/src/pydata_sphinx_theme/assets/styles/sections/_header.scss +++ b/src/pydata_sphinx_theme/assets/styles/sections/_header.scss @@ -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 { @@ -169,7 +197,9 @@ background-color: inherit; border: none; - @include icon-navbar-hover; + &:hover { + @include icon-hover; + } } button.primary-toggle { 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..77022b9af 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 %}