All files / js theme-toggle.js

0% Statements 0/58
0% Branches 0/48
0% Functions 0/9
0% Lines 0/54

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137                                                                                                                                                                                                                                                                                 
/**
 * @module ThemeToggle
 * @description Dark/light theme toggle for Riksdagsmonitor.
 *
 * Behaviour:
 *  1. On first visit, reads `prefers-color-scheme` to pick dark or light.
 *  2. The user's explicit choice is persisted to `localStorage`.
 *  3. The `data-theme` attribute on `<html>` drives all CSS custom-property
 *     overrides, giving higher specificity than the media-query fallback.
 *  4. Listens for system-theme changes and follows them unless the user has
 *     already made an explicit choice.
 *
 * Accessibility:
 *  - Toggle is a native `<button>` with `aria-pressed` and a descriptive
 *    `aria-label` that updates on every toggle.
 *  - Keyboard: native `<button>` handles Enter / Space automatically.
 *
 * @license Apache-2.0
 */
(function () {
  'use strict';
 
  const STORAGE_KEY = 'riksdagsmonitor-theme';
  const DARK  = 'dark';
  const LIGHT = 'light';
 
  /* ── Helpers ──────────────────────────────────────────────────────────── */
 
  function prefersDark() {
    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
  }
 
  /**
   * Determine the initial theme.
   * Priority: localStorage > system preference
   */
  function resolveTheme() {
    try {
      const saved = localStorage.getItem(STORAGE_KEY);
      if (saved === DARK || saved === LIGHT) return saved;
    } catch (_) { /* private browsing */ }
    return prefersDark() ? DARK : LIGHT;
  }
 
  /**
   * Write the theme to the DOM and persist it.
   * @param {string} theme - 'dark' | 'light'
   * @param {boolean} [persist=true] - save to localStorage
   */
  function applyTheme(theme, persist) {
    document.documentElement.setAttribute('data-theme', theme);
    if (persist !== false) {
      try { localStorage.setItem(STORAGE_KEY, theme); } catch (_) { /* storage unavailable */ }
    }
  }
 
  /* ── Button state ─────────────────────────────────────────────────────── */
 
  function updateButton(theme) {
    const btn = document.getElementById('theme-toggle');
    if (!btn) return;
 
    const isDark  = theme === DARK;
    const icon    = btn.querySelector('.theme-icon');
    const darkLbl = btn.getAttribute('data-label-dark')  || 'Switch to light theme';
    const lightLbl= btn.getAttribute('data-label-light') || 'Switch to dark theme';
 
    btn.setAttribute('aria-pressed', String(isDark));
    btn.setAttribute('aria-label',   isDark ? darkLbl : lightLbl);
    btn.setAttribute('title',        isDark ? darkLbl : lightLbl);
 
    if (icon) icon.textContent = isDark ? '☀️' : '🌙';
  }
 
  /* ── Toggle handler ───────────────────────────────────────────────────── */
 
  function toggle() {
    const current = document.documentElement.getAttribute('data-theme') || LIGHT;
    const next    = current === DARK ? LIGHT : DARK;
    applyTheme(next);
    updateButton(next);
  }
 
  /* ── Boot ─────────────────────────────────────────────────────────────── */
 
  // Apply before first paint (called synchronously by the anti-flash snippet
  // already present in <head>; this line covers when the module loads later).
  var _initialTheme = resolveTheme();
  applyTheme(_initialTheme, false /* initial resolution only; do not persist on module boot */);
  // Update button immediately — the script is placed after the button in <body>
  // so the element is already in the DOM; this avoids a brief mismatch before
  // DOMContentLoaded fires.
  updateButton(_initialTheme);
 
  document.addEventListener('DOMContentLoaded', function () {
    const theme = document.documentElement.getAttribute('data-theme') || LIGHT;
    updateButton(theme);
 
    const btn = document.getElementById('theme-toggle');
    if (btn) {
      btn.addEventListener('click', toggle);
    }
 
    // Follow system changes only when no explicit user preference is set
    if (window.matchMedia) {
      var mql = window.matchMedia('(prefers-color-scheme: dark)');
      var handleSchemeChange = function (e) {
        try {
          var storedTheme = localStorage.getItem(STORAGE_KEY);
          if (storedTheme === DARK || storedTheme === LIGHT) {
            return; // explicit valid choice wins
          }
          // Clear invalid or legacy values so system preference can apply
          if (storedTheme !== null) {
            localStorage.removeItem(STORAGE_KEY);
          }
        } catch (_) { /* storage unavailable */ }
        var sysTheme = e.matches ? DARK : LIGHT;
        applyTheme(sysTheme, false);
        updateButton(sysTheme);
      };
      // Use addEventListener where available; fall back to legacy addListener
      if (typeof mql.addEventListener === 'function') {
        mql.addEventListener('change', handleSchemeChange);
      } else if (typeof mql.addListener === 'function') {
        mql.addListener(handleSchemeChange);
      }
    }
  });
 
  // Expose for programmatic use (e.g. Chart.js colour refresh)
  window.riksdagsToggleTheme  = toggle;
  window.riksdagsGetTheme     = function () {
    return document.documentElement.getAttribute('data-theme') || LIGHT;
  };
})();