Skip to content
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

Degrade gracefully when JavaScript is disabled #1146

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
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(`
12rambau marked this conversation as resolved.
Show resolved Hide resolved
<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 %}>
12rambau marked this conversation as resolved.
Show resolved Hide resolved
{% 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