diff --git a/src/ethicalads.js b/src/ethicalads.js index 0a3c0310..28b75d12 100644 --- a/src/ethicalads.js +++ b/src/ethicalads.js @@ -312,6 +312,7 @@ export class EthicalAdsAddon extends AddonBase { if (placement !== null) { // Allow EA to switch between light/dark mode placement.classList.add("adaptive-css"); + this.syncDarkModeWithPageColorScheme(placement); // This ensure us that all the `data-ea-*` attributes are already set in the HTML tag. placement.setAttribute("data-ea-manual", "true"); @@ -391,6 +392,35 @@ export class EthicalAdsAddon extends AddonBase { return placement; } + syncDarkModeWithPageColorScheme(placement) { + // Material for MkDocs, Zensical, and sphinx-immaterial signal their color + // scheme with a `data-md-color-scheme` attribute ("slate" is dark), which + // the EthicalAds client doesn't recognize in its `adaptive-css` mode. + // Toggle the client's explicit `dark` class to match the page. + const element = document.querySelector( + "html[data-md-color-scheme], body[data-md-color-scheme]", + ); + if (!element) { + return; + } + + const updateDarkClass = () => { + if (element.getAttribute("data-md-color-scheme") === "slate") { + placement.classList.add("dark"); + } else { + placement.classList.remove("dark"); + } + }; + updateDarkClass(); + + // Keep the ad in sync when the user or the OS switches modes. + const observer = new MutationObserver(updateDarkClass); + observer.observe(element, { + attributes: true, + attributeFilter: ["data-md-color-scheme"], + }); + } + elementAboveTheFold(element) { // Return false if element doesn't exist if (!element) { diff --git a/tests/ethicalads.test.js b/tests/ethicalads.test.js index 635dc157..6467726a 100644 --- a/tests/ethicalads.test.js +++ b/tests/ethicalads.test.js @@ -1,6 +1,65 @@ -import { expect, assert, fixture, html } from "@open-wc/testing"; +import { expect, assert, fixture, html, aTimeout } from "@open-wc/testing"; import { EthicalAdsAddon } from "../src/ethicalads"; +describe("EthicalAds addon dark mode", () => { + const config = { + addons: { + ethicalads: { + enabled: true, + ad_free: false, + campaign_types: ["community", "paid"], + keywords: ["docs"], + publisher: "readthedocs", + }, + }, + }; + + afterEach(() => { + // Remove the elements injected by the addon so each test starts clean. + const script = document.querySelector("#ethicaladsjs"); + if (script) { + script.remove(); + } + for (const placement of document.querySelectorAll("[data-ea-publisher]")) { + placement.remove(); + } + document.body.removeAttribute("data-md-color-scheme"); + }); + + it("adds the dark class when the page uses Material's slate color scheme", () => { + document.body.setAttribute("data-md-color-scheme", "slate"); + + new EthicalAdsAddon(config); + + const placement = document.querySelector("[data-ea-publisher]"); + expect(placement.classList.contains("dark")).to.be.true; + }); + + it("does not add the dark class when the page has no color scheme attribute", () => { + new EthicalAdsAddon(config); + + const placement = document.querySelector("[data-ea-publisher]"); + expect(placement.classList.contains("dark")).to.be.false; + }); + + it("toggles the dark class when the page color scheme changes", async () => { + document.body.setAttribute("data-md-color-scheme", "default"); + + new EthicalAdsAddon(config); + + const placement = document.querySelector("[data-ea-publisher]"); + expect(placement.classList.contains("dark")).to.be.false; + + document.body.setAttribute("data-md-color-scheme", "slate"); + await aTimeout(0); + expect(placement.classList.contains("dark")).to.be.true; + + document.body.setAttribute("data-md-color-scheme", "default"); + await aTimeout(0); + expect(placement.classList.contains("dark")).to.be.false; + }); +}); + describe("EthicalAds addon", () => { it("invalid configuration disables the addon", () => { expect(