All files / src/components/ThemeProvider theme-provider.ts

97.29% Statements 36/37
61.53% Branches 8/13
100% Functions 8/8
96% Lines 24/25

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                                              73x 73x 73x 292x                                 387x               387x     216x 353x 216x             216x 353x 353x 353x 353x 353x   353x 216x                               216x       216x 353x       353x 216x       706x 706x                    
import { LitElement, unsafeCSS, html, PropertyValueMap } from "lit"
import { customElement, property } from "lit/decorators.js"
import theme from "./theme.scss?inline"
 
/**
 * Supported theme names that can be applied by {@link ThemeProvider}.
 */
export type ThemeMode = "dark" | "light" | "glass" | "cartoon"
 
/**
 * Detail payload for the {@link ThemeProvider | theme provider}'s `change` event.
 */
export interface ThemeChangeDetail {
    /**
     * Theme mode that is now active on the provider.
     */
    readonly mode: ThemeMode
    /**
     * Theme mode that was active before the change. `null` when no previous theme existed.
     */
    readonly previousMode: ThemeMode | null
}
 
const THEME_CLASS_PREFIX = "vg-theme-"
const SUPPORTED_THEMES: readonly ThemeMode[] = ["dark", "light", "glass", "cartoon"]
const SUPPORTED_THEME_SET = new Set<ThemeMode>(SUPPORTED_THEMES)
const THEME_CLASSES = SUPPORTED_THEMES.map((mode) => `${THEME_CLASS_PREFIX}${mode}`)
 
/**
 * Theme provider that exposes vg design tokens as CSS variables and toggles between predefined modes.
 *
 * @tag vg-theme-provider
 * @tagname vg-theme-provider
 * @summary The ThemeProvider component, used for managing application-wide theme and design tokens.
 *
 * @slot - Components that should inherit the active theme and design tokens
 *
 * @csspart provider - Allows you to style the theme provider container
 *
 * @fires {ThemeChangeDetail} vg-change - Emitted whenever the active theme mode changes
 *
 */
@customElement("vg-theme-provider")
export class ThemeProvider extends LitElement {
    static styles = unsafeCSS(theme)
 
    /**
     * Theme mode that controls which token values are exposed.
     * The value is normalised to one of ThemeMode. Unsupported values fall back to "dark".
     */
    @property({ type: String, reflect: true })
    public mode: ThemeMode = "dark"
 
    protected willUpdate(changedProperties: PropertyValueMap<this>) {
        if (changedProperties.has("mode")) {
            const normalised = this.normaliseTheme(this.mode)
            Iif (Inormalised !== this.mode) {
                this.mode = normalised
            }
        }
    }
 
    protected updated(changedProperties: PropertyValueMap<this>) {
        if (changedProperties.has("mode")) {
            const previousValue = changedProperties.get("mode") as ThemeMode | undefined
            const previousMode = previousValue === undefined ? null : this.normaliseTheme(previousValue)
            const nextMode = this.normaliseTheme(this.mode)
            this.applyThemeClass(nextMode)
            this.setThemeAttributes(nextMode)
 
            if (previousMode !== nextMode) {
                this.dispatchEvent(new CustomEvent<ThemeChangeDetail>("vg-change", {
                    detail: {
                        mode: nextMode,
                        previousMode,
                    },
                    // bubbles: true,
                    composed: true,
                }))
            }
        }
    }
 
    /**
     * @inheritdoc
     */
    render() {
        return html`<slot></slot>`
    }
 
    private applyThemeClass(mode: ThemeMode) {
        this.classList.remove(...THEME_CLASSES)
        this.classList.add(`${THEME_CLASS_PREFIX}${mode}`)
    }
 
    private setThemeAttributes(mode: ThemeMode) {
        this.dataset.vgTheme = mode
        this.setAttribute("data-vg-theme", mode)
    }
 
    private normaliseTheme(value: ThemeMode | string | undefined): ThemeMode {
        const candidate = (value ?? "").toString().toLowerCase() as ThemeMode
        return SUPPORTED_THEME_SET.has(candidate) ? candidate : "dark"
    }
}
 
 
declare global {
    interface HTMLElementTagNameMap {
        'vg-theme-provider': ThemeProvider
    }
}