Skip to content

Commit

Permalink
Degrade gracefully when JavaScript is disabled (#1146)
Browse files Browse the repository at this point in the history
  • Loading branch information
lunar-debian authored Feb 9, 2023
1 parent df125aa commit fb03af0
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 99 deletions.
100 changes: 59 additions & 41 deletions src/pydata_sphinx_theme/assets/styles/variables/_color.scss
Original file line number Diff line number Diff line change
Expand Up @@ -80,53 +80,71 @@ $pst-semantic-colors: (

/*******************************************************************************
* write the color rules for each theme (light/dark)
*
* NOTE: @each {...} is like a for-loop
* https://sass-lang.com/documentation/at-rules/control/each
* and #{...} inserts a variable into a CSS selector or property name
* https://sass-lang.com/documentation/interpolation
*/
@each $mode in (light, dark) {
html[data-theme="#{$mode}"] {
@each $name, $value in $pst-semantic-colors {
// check if this color is defined differently for light/dark
@if type-of($value) == map {
$value: map-get($value, $mode);
}

/* NOTE:
* Mixins enable us to reuse the same definitions for the different modes
* https://sass-lang.com/documentation/at-rules/mixin
* #{...} inserts a variable into a CSS selector or property name
* https://sass-lang.com/documentation/interpolation
*/
@mixin theme-colors($mode) {
// check if this color is defined differently for light/dark
@each $name, $value in $pst-semantic-colors {
@if type-of($value) == map {
$value: map-get($value, $mode);
}
& {
--pst-color-#{$name}: #{$value};
}
// assign the "duplicate" colors (ones that just reference other variables)
}
// assign the "duplicate" colors (ones that just reference other variables)
& {
--pst-color-link: var(--pst-color-primary);
--pst-color-link-hover: var(--pst-color-warning);
// adapt to light/dark-specific content
@if $mode == "light" {
.only-dark {
display: none !important;
}
} @else {
.only-light {
display: none !important;
}
/* Adjust images in dark mode (unless they have class .only-dark or
* .dark-light, in which case assume they're already optimized for dark
* mode).
*/
img:not(.only-dark):not(.dark-light) {
filter: brightness(0.8) contrast(1.2);
}
/* Give images a light background in dark mode in case they have
* transparency and black text (unless they have class .only-dark or .dark-light, in
* which case assume they're already optimized for dark mode).
*/
.bd-content img:not(.only-dark):not(.dark-light) {
background: rgb(255, 255, 255);
border-radius: 0.25rem;
}
// MathJax SVG outputs should be filled to same color as text.
.MathJax_SVG * {
fill: var(--pst-color-text-base);
}
}
// adapt to light/dark-specific content
@if $mode == "light" {
.only-dark {
display: none !important;
}
} @else {
.only-light {
display: none !important;
}
/* Adjust images in dark mode (unless they have class .only-dark or
* .dark-light, in which case assume they're already optimized for dark
* mode).
*/
img:not(.only-dark):not(.dark-light) {
filter: brightness(0.8) contrast(1.2);
}
/* Give images a light background in dark mode in case they have
* transparency and black text (unless they have class .only-dark or .dark-light, in
* which case assume they're already optimized for dark mode).
*/
.bd-content img:not(.only-dark):not(.dark-light) {
background: rgb(255, 255, 255);
border-radius: 0.25rem;
}
// MathJax SVG outputs should be filled to same color as text.
.MathJax_SVG * {
fill: var(--pst-color-text-base);
}
}
}

/* Defaults to light mode if data-theme is not set */
html:not([data-theme]) {
@include theme-colors("light");
}

/* NOTE: @each {...} is like a for-loop
* https://sass-lang.com/documentation/at-rules/control/each
*/
@each $mode in (light, dark) {
html[data-theme="#{$mode}"] {
@include theme-colors($mode);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@
{% set is_logo = "light" in theme_logo["image_relative"] %}
{% set alt = theme_logo.get("alt_text", "Logo image") %}
{% if is_logo %}
<img src="{{ theme_logo['image_relative']['light'] }}" class="logo__image only-light" alt="{{ alt }}"/>
<img src="{{ theme_logo['image_relative']['dark'] }}" class="logo__image only-dark" alt="{{ alt }}"/>
{# Theme switching is only available when JavaScript is enabled.
# Thus we should add the extra image using JavaScript, defaulting
# depending on the value of default_mode; and light if unset.
#}
{% if default_mode is undefined %}
{% set default_mode = "light" %}
{% endif %}
{% set js_mode = "light" if default_mode == "dark" else "dark" %}
<img src="{{ theme_logo['image_relative'][default_mode] }}" class="logo__image only-{{ default_mode }}" alt="{{ alt }}"/>
<script>document.write(`<img src="{{ theme_logo['image_relative'][js_mode] }}" class="logo__image only-{{ js_mode }}" alt="{{ alt }}"/>`);</script>
{% endif %}
{% if not is_logo or theme_logo.get("text") %}
<p class="title logo__title">{{ theme_logo.get("text") or docstitle }}</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{# A button that, when clicked, will trigger a search popup overlay #}
<button class="btn btn-sm navbar-btn search-button search-button__button" title="{{ _('Search') }}" aria-label="{{ _('Search') }}" data-bs-placement="bottom" data-bs-toggle="tooltip">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
{# A button that, when clicked, will trigger a search popup overlay.
#
# As this function will only work when JavaScript is enabled, we add it through JavaScript.
#}
<script>
document.write(`
<button class="btn btn-sm navbar-btn search-button search-button__button" title="{{ _('Search') }}" aria-label="{{ _('Search') }}" data-bs-placement="bottom" data-bs-toggle="tooltip">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
`);
</script>
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<button class="theme-switch-button btn btn-sm btn-outline-primary navbar-btn rounded-circle" title="{{ _('light/dark') }}" aria-label="{{ _('light/dark') }}" data-bs-placement="bottom" data-bs-toggle="tooltip">
{# As the theme switcher will only work when JavaScript is enabled, we add it through JavaScript.
#}
<script>
document.write(`
<button class="theme-switch-button btn btn-sm btn-outline-primary navbar-btn rounded-circle" title="{{ _('light/dark') }}" aria-label="{{ _('light/dark') }}" data-bs-placement="bottom" data-bs-toggle="tooltip">
<span class="theme-switch" data-mode="light"><i class="fa-solid fa-sun"></i></span>
<span class="theme-switch" data-mode="dark"><i class="fa-solid fa-moon"></i></span>
<span class="theme-switch" data-mode="auto"><i class="fa-solid fa-circle-half-stroke"></i></span>
</button>
</button>
`);
</script>
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<div class="version-switcher__container dropdown">
{# As the version switcher will only work when JavaScript is enabled, we add it through JavaScript.
#}
<script>
document.write(`
<div class="version-switcher__container dropdown">
<button type="button" class="version-switcher__button btn btn-sm navbar-btn dropdown-toggle" data-bs-toggle="dropdown">
{{ theme_switcher.get('version_match') }} <!-- this text may get changed later by javascript -->
<span class="caret"></span>
{{ theme_switcher.get('version_match') }} <!-- this text may get changed later by javascript -->
<span class="caret"></span>
</button>
<div class="version-switcher__menu dropdown-menu list-group-flush py-0">
<!-- dropdown will be populated by javascript on page load -->
</div>
</div>
</div>
`);
</script>
11 changes: 7 additions & 4 deletions src/pydata_sphinx_theme/theme/pydata_sphinx_theme/layout.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{# We redefine <html/> for "basic/layout.html" to add a default `data-theme` attribute when
# a default mode has been set. This also improves compatibility when JavaScript is disabled.
#}
{% set html_tag %}
<html{% if not html5_doctype %} xmlns="http://www.w3.org/1999/xhtml"{% endif %}{% if language is not none %} lang="{{ language }}"{% endif %} {% if default_mode %}data-theme="{{ default_mode }}"{% endif %}>
{% endset %}
{%- extends "basic/layout.html" %}
{%- import "static/webpack-macros.html" as _webpack with context %}
{# Metadata and asset linking #}
Expand Down Expand Up @@ -64,10 +70,7 @@
<div class="search-button__search-container">{% include "../components/search-field.html" %}</div>
</div>
{%- if theme_announcement -%}
<div class="bd-header-announcement container-fluid"
id="header-announcement">
{% include "sections/announcement.html" %}
</div>
{% include "sections/announcement.html" %}
{%- endif %}
{% block docs_navbar %}
<nav class="bd-header navbar navbar-expand-lg bd-navbar" id="navbar-main">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{% set header_classes = ["bd-header-announcement", "container-fluid"] %}
{% set is_remote=theme_announcement.startswith("http") %}
{# If we are remote, add a script to make an HTTP request for the value on page load #}
{%- if is_remote %}
<script>
document.write(`<div id="header-announcement"></div>`);
fetch("{{ theme_announcement }}")
.then(res => {return res.text();})
.then(data => {
div = document.querySelector("#header-announcement");
div.classList.add(...{{ header_classes | tojson }});
div.innerHTML = `<div class="bd-header-announcement__content">${data}</div>`;
})
.catch(error => {
Expand All @@ -14,5 +17,7 @@
</script>
{#- if announcement text is not remote, populate announcement w/ local content -#}
{%- else %}
<div class="bd-header-announcement__content">{{ theme_announcement }}</div>
<div class="{{ header_classes | join(' ') }}" id="header-announcement">
<div class="bd-header-announcement__content">{{ theme_announcement }}</div>
</div>
{% endif %}
55 changes: 53 additions & 2 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,51 @@ def test_logo_two_images(sphinx_build_factory):
assert "Foo Title" in index_str


def test_primary_logo_is_light_when_no_default_mode(sphinx_build_factory):
"""Test that the primary logo image is light
(and secondary, written through JavaScript, is dark)
when no default mode is set."""
# Ensure no default mode is set
confoverrides = {
"html_context": {},
}
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
index_html = sphinx_build.html_tree("index.html")
navbar_brand = index_html.select(".navbar-brand")[0]
assert navbar_brand.find("img", class_="only-light") is not None
assert navbar_brand.find("script", string=re.compile("only-dark")) is not None


def test_primary_logo_is_light_when_default_mode_is_light(sphinx_build_factory):
"""Test that the primary logo image is light
(and secondary, written through JavaScript, is dark)
when default mode is set to light."""
# Ensure no default mode is set
confoverrides = {
"html_context": {"default_mode": "light"},
}
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
index_html = sphinx_build.html_tree("index.html")
navbar_brand = index_html.select(".navbar-brand")[0]
assert navbar_brand.find("img", class_="only-light") is not None
assert navbar_brand.find("script", string=re.compile("only-dark")) is not None


def test_primary_logo_is_dark_when_default_mode_is_dark(sphinx_build_factory):
"""Test that the primary logo image is dark
(and secondary, written through JavaScript, is light)
when default mode is set to dark."""
# Ensure no default mode is set
confoverrides = {
"html_context": {"default_mode": "dark"},
}
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
index_html = sphinx_build.html_tree("index.html")
navbar_brand = index_html.select(".navbar-brand")[0]
assert navbar_brand.find("img", class_="only-dark") is not None
assert navbar_brand.find("script", string=re.compile("only-light")) is not None


def test_logo_missing_image(sphinx_build_factory):
"""Test that a missing image will raise a warning."""
# Test with a specified title and a dark logo
Expand Down Expand Up @@ -665,7 +710,9 @@ def test_version_switcher(sphinx_build_factory, file_regression, url):

if url == "switcher.json": # this should work
index = sphinx_build.html_tree("index.html")
switcher = index.select(".version-switcher__container")[0]
switcher = index.select(".navbar-header-items")[0].find(
"script", string=re.compile(".version-switcher__container")
)
file_regression.check(
switcher.prettify(), basename="navbar_switcher", extension=".html"
)
Expand All @@ -683,7 +730,11 @@ def test_theme_switcher(sphinx_build_factory, file_regression):
"""Regression test the theme switcher btn HTML"""

sphinx_build = sphinx_build_factory("base").build()
switcher = sphinx_build.html_tree("index.html").select(".theme-switch-button")[0]
switcher = (
sphinx_build.html_tree("index.html")
.find(string=re.compile("theme-switch-button"))
.find_parent("script")
)
file_regression.check(
switcher.prettify(), basename="navbar_theme", extension=".html"
)
Expand Down
24 changes: 13 additions & 11 deletions tests/test_build/navbar_switcher.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<div class="version-switcher__container dropdown">
<button class="version-switcher__button btn btn-sm navbar-btn dropdown-toggle" data-bs-toggle="dropdown" type="button">
0.7.1
<!-- this text may get changed later by javascript -->
<span class="caret">
</span>
</button>
<div class="version-switcher__menu dropdown-menu list-group-flush py-0">
<!-- dropdown will be populated by javascript on page load -->
</div>
</div>
<script>
document.write(`
<div class="version-switcher__container dropdown">
<button type="button" class="version-switcher__button btn btn-sm navbar-btn dropdown-toggle" data-bs-toggle="dropdown">
0.7.1 <!-- this text may get changed later by javascript -->
<span class="caret"></span>
</button>
<div class="version-switcher__menu dropdown-menu list-group-flush py-0">
<!-- dropdown will be populated by javascript on page load -->
</div>
</div>
`);
</script>
23 changes: 9 additions & 14 deletions tests/test_build/navbar_theme.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
<button aria-label="light/dark" class="theme-switch-button btn btn-sm btn-outline-primary navbar-btn rounded-circle" data-bs-placement="bottom" data-bs-toggle="tooltip" title="light/dark">
<span class="theme-switch" data-mode="light">
<i class="fa-solid fa-sun">
</i>
</span>
<span class="theme-switch" data-mode="dark">
<i class="fa-solid fa-moon">
</i>
</span>
<span class="theme-switch" data-mode="auto">
<i class="fa-solid fa-circle-half-stroke">
</i>
</span>
</button>
<script>
document.write(`
<button class="theme-switch-button btn btn-sm btn-outline-primary navbar-btn rounded-circle" title="light/dark" aria-label="light/dark" data-bs-placement="bottom" data-bs-toggle="tooltip">
<span class="theme-switch" data-mode="light"><i class="fa-solid fa-sun"></i></span>
<span class="theme-switch" data-mode="dark"><i class="fa-solid fa-moon"></i></span>
<span class="theme-switch" data-mode="auto"><i class="fa-solid fa-circle-half-stroke"></i></span>
</button>
`);
</script>
23 changes: 9 additions & 14 deletions tests/test_build/sidebar_subpage.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,15 @@
</div>
<div class="sidebar-header-items__end">
<div class="navbar-end-item">
<button aria-label="light/dark" class="theme-switch-button btn btn-sm btn-outline-primary navbar-btn rounded-circle" data-bs-placement="bottom" data-bs-toggle="tooltip" title="light/dark">
<span class="theme-switch" data-mode="light">
<i class="fa-solid fa-sun">
</i>
</span>
<span class="theme-switch" data-mode="dark">
<i class="fa-solid fa-moon">
</i>
</span>
<span class="theme-switch" data-mode="auto">
<i class="fa-solid fa-circle-half-stroke">
</i>
</span>
</button>
<script>
document.write(`
<button class="theme-switch-button btn btn-sm btn-outline-primary navbar-btn rounded-circle" title="light/dark" aria-label="light/dark" data-bs-placement="bottom" data-bs-toggle="tooltip">
<span class="theme-switch" data-mode="light"><i class="fa-solid fa-sun"></i></span>
<span class="theme-switch" data-mode="dark"><i class="fa-solid fa-moon"></i></span>
<span class="theme-switch" data-mode="auto"><i class="fa-solid fa-circle-half-stroke"></i></span>
</button>
`);
</script>
</div>
<div class="navbar-end-item">
<ul aria-label="Icon Links" class="navbar-nav" id="navbar-icon-links">
Expand Down

0 comments on commit fb03af0

Please sign in to comment.