• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

palcarazm / bs-darkmode-toggle / 24964220831

26 Apr 2026 06:44PM UTC coverage: 97.258% (-0.9%) from 98.182%
24964220831

Pull #86

github

web-flow
Merge 60ffc03a3 into f415e88c7
Pull Request #86: v1.1.0

284 of 299 branches covered (94.98%)

Branch coverage included in aggregate %.

133 of 134 new or added lines in 9 files covered. (99.25%)

1 existing line in 1 file now uncovered.

319 of 321 relevant lines covered (99.38%)

120.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.33
/src/main/ts/DarkModeToggle.ts
1
import { OptionResolver } from "./core/OptionResolver";
4✔
2
import { StateReducer } from "./core/StateReducer";
4✔
3
import { StorageManager } from "./core/storage/StorageManager";
4✔
4
import { DomManager } from "./core/dom/DomManager";
4✔
5
import { ResolvedOptions, StorageType } from "./core/OptionResolver.types";
6
import { ActionType } from "./core/StateReducer.types";
4✔
7
import { ColorModes } from "./types/ColorModes";
4✔
8
import { EventFactory } from "./core/events/EventFactory";
4✔
9
import { CustomEventTypes, PrefixedCustomEventTypes } from "./core/events/Events.types";
4✔
10
import type { DarkModeToggleEventMap } from "./core/events/Events.types";
11
import { Component } from "component-lifecycle";
4✔
12

13
export class DarkModeToggle extends Component<"darkmode", DarkModeToggleEventMap> {
4✔
14
    protected readonly PREFIX = "darkmode";
152✔
15
    private readonly toggleOptions: ResolvedOptions;
16
    private readonly toggleState: StateReducer;
17
    private storage?: StorageManager;
18
    private dom?: DomManager;
19

20
    constructor(element: HTMLElement, opts = {}) {
4✔
21
        super(element);
152✔
22

23
        this.toggleOptions = OptionResolver.resolve(element, opts);
152✔
24
        this.toggleState = new StateReducer(this.toggleOptions.state, this.toggleOptions.lightColorMode, this.toggleOptions.darkColorMode);
152✔
25
    }
26

27
    /**
28
     * Factory method to create an instance of DarkModeToggle.
29
     * @param element the root element for the dark mode toggle component. The component will look for configuration options in this element's attributes.
30
     * @param opts the user provided options to configure the dark mode toggle instance. These options will override any configuration found in the element's attributes.
31
     * @returns A promise that resolves to the created and initialized DarkModeToggle instance
32
     */
33
    static async create(element: HTMLElement, opts = {}): Promise<DarkModeToggle> {
144✔
34
        const instance = new DarkModeToggle(element, opts);
144✔
35
        await instance.init();
144✔
36
        await instance.attach();
144✔
37
        return instance;
144✔
38
    }
39

40
    protected async doInit(): Promise<{ cancelled: boolean; reason?: string }> {
41
        this.storage = new StorageManager(this.toggleOptions.storage);
152✔
42
        this.applyPreferredScheme();
152✔
43
        return { cancelled: false };
152✔
44
    }
45

46
    protected async doAttach(): Promise<{ cancelled: boolean; reason?: string }> {
47
        const dom = new DomManager(this.element, this.toggleOptions, (e) => {
152✔
NEW
48
            this.toggle();
×
UNCOV
49
            e.preventDefault();
×
50
        });
51

52
        this.dom = dom;
152✔
53

54
        this.element._bsDarkmodeToggle = this;
152✔
55

56
        this.setupCrossInstanceSync();
152✔
57
        this.syncState();
152✔
58
        return { cancelled: false };
152✔
59
    }
60

61
    protected async doDispose(): Promise<{ cancelled: boolean; reason?: string }> {
62
        globalThis.document.removeEventListener(PrefixedCustomEventTypes.CHANGE, this.handleExternalThemeChange);
152✔
63
        return { cancelled: false };
152✔
64
    }
65

66
    protected async doDestroy(): Promise<{ cancelled: boolean; reason?: string }> {
67
        if(this.isAttached()) await this.dispose();
152!
68
        this.dom?.destroy();
152!
69
        delete this.element._bsDarkmodeToggle;
152✔
70
        return { cancelled: false };
152✔
71
    }
72

73
    /**
74
     * Sets up an event listener to handle external theme change events.
75
     * When an external theme change event is triggered, this method updates the control state.
76
     * @private
77
     */
78
    private setupCrossInstanceSync(){
79
        globalThis.document.addEventListener(PrefixedCustomEventTypes.CHANGE, this.handleExternalThemeChange);
152✔
80
    }
81

82
    /**
83
     * Handles an external theme change event by updating the state and the DOM
84
     * if the root elements of the event and the component share roots.
85
     * 
86
     * Implementation note: for performance reasons, DOM is only updated when the state is updated.
87
     * @private
88
     * @param e - The external theme change event
89
     */
90
    private readonly handleExternalThemeChange = (e: Event) =>{
152✔
91
        const detail = (e as CustomEvent)?.detail;
24!
92
        if (!detail || typeof detail.isLight !== "boolean" || !Array.isArray(detail.roots)) {
24✔
93
            return;
4✔
94
        }
95
        const { isLight, roots: eventRoots } = detail;
20✔
96
        
97
        const thisRoots = this.dom?.roots;
20!
98
        const allRootsAffected = thisRoots?.every(root => eventRoots.includes(root));
36✔
99
        
100
        if (allRootsAffected && this.toggleState.do(ActionType.OVERRIDE, { isLight })) {
20✔
101
            this.dom?.setState(this.toggleState.get());
8!
102
        }
103
    };
104

105
    /**
106
     * Syncs the state of the dark mode toggle by updating the DOM and persisting the current theme to storage.
107
     * @private
108
     */
109
    private syncState() {
110
        this.dom?.setState(this.toggleState.get());
176!
111
        this.persistTheme();
176✔
112
    }
113

114
    toggle(silent = false) {
6✔
115
        this.ensureNotDestroyed();
16✔
116
        if(!this.toggleState.do(ActionType.TOGGLE)) return;
12✔
117
        this.syncState();
8✔
118
        this.trigger(silent);
8✔
119
    }
120

121
    light(silent = false) {
6✔
122
        this.ensureNotDestroyed();
16✔
123
        if(!this.toggleState.do(ActionType.LIGHT)) return;
12✔
124
        this.syncState();
8✔
125
        this.trigger(silent);
8✔
126
    }
127

128
    dark(silent = false) {
6✔
129
        this.ensureNotDestroyed();
16✔
130
        if(!this.toggleState.do(ActionType.DARK)) return;
12✔
131
        this.syncState();
8✔
132
        this.trigger(silent);
8✔
133
    }
134

135
    setStorageType(type: StorageType) {
136
        this.ensureNotDestroyed();
8✔
137
        this.storage?.setStorageType(type);
4!
138
        this.persistTheme();
4✔
139
    }
140

141
    /**
142
     * Triggers the events if silent is false.
143
     * The events are triggered with the current state of the dark mode toggle.
144
     * Emits the typed event via Component.emit and dispatches the legacy event manually.
145
     * @private
146
     * @param {boolean} silent - Whether to trigger the event.
147
     */
148
    private trigger(silent: boolean) {
149
        if (silent) return;
24✔
150

151
        const legacyEvent = EventFactory.createLegacyEvent();
12✔
152
        this.element.dispatchEvent(legacyEvent);
12✔
153

154
        const roots = this.dom?.roots || [];
12!
155
        const currentState = this.toggleState.get();
12✔
156
        const eventDetail = EventFactory.createEventDetail(currentState, this.element, roots);
12✔
157
        this.emit(CustomEventTypes.CHANGE, eventDetail);
12✔
158
        roots.forEach((root) => {
12✔
159
            root.dispatchEvent(EventFactory.createPrefixedEvent(currentState, this.element, roots));
12✔
160
        });
161
    }
162

163
    /**
164
     * Persist the current theme to the storage.
165
     * @private
166
     */
167
    private persistTheme() {
168
        this.storage?.set(this.toggleState.get().theme);
180!
169
    }
170

171
    /**
172
     * Applies the preferred color scheme based on cookies or system preference
173
     * @returns a boolean indicating whether a preference was applied (true) or not (false)
174
     */
175
    private applyPreferredScheme(): boolean {
176
        return this.applyStoredPreference() || this.applySystemPreference();
152✔
177
    }
178

179
    /**
180
     * Applies the color scheme based on stored preference if available
181
     * @returns a boolean indicating whether a preference was applied (true) or not (false)
182
     */
183
    private applyStoredPreference(): boolean {
184
        const value = this.storage?.get();
152!
185

186
        if (value === this.toggleOptions.darkColorMode) {
152✔
187
            this.toggleState.do(ActionType.DARK);
4✔
188
            return true;
4✔
189
        }
190
        if (value === this.toggleOptions.lightColorMode) {
148✔
191
            this.toggleState.do(ActionType.LIGHT);
4✔
192
            return true;
4✔
193
        }
194
        return false;
144✔
195
    }
196

197
    /**
198
     * Applies the color scheme based on system preferences if available
199
     * @returns a boolean indicating whether a preference was applied (true) or not (false)
200
     */
201
    private applySystemPreference(): boolean {
202
        const systemPreference = this.getSystemPreference();
144✔
203
        if (systemPreference === ColorModes.DARK) {
144✔
204
            this.toggleState.do(ActionType.DARK);
8✔
205
            return true;
8✔
206
        }
207
        if (systemPreference === ColorModes.LIGHT) {
136✔
208
            this.toggleState.do(ActionType.LIGHT);
8✔
209
            return true;
8✔
210
        }
211
        return false;
128✔
212
    }
213

214
    /**
215
     * Gets the system color scheme preference if available
216
     * @returns color scheme preference as `ColorModes`
217
     */
218
    private getSystemPreference(): ColorModes {
219
        try {
168✔
220
            const darkModeQuery = globalThis.window?.matchMedia("(prefers-color-scheme: dark)");
168!
221
            const lightModeQuery = globalThis.window?.matchMedia("(prefers-color-scheme: light)");
148!
222
            
223
            if (darkModeQuery?.matches) {
148✔
224
                return ColorModes.DARK;
12✔
225
            }
226
            if (lightModeQuery?.matches) {
136✔
227
                return ColorModes.LIGHT;
12✔
228
            }
229
            return ColorModes.NONE;
124✔
230
        } catch (error) {
231
            console.warn("Unable to detect system color scheme preference:", error);
20✔
232
            return ColorModes.NONE;
20✔
233
        }
234
    }
235

236
    /**
237
     * Checks if the bs-darkmode-toggle instance has been destroyed.
238
     * If it has, throws an error indicating that the instance is no longer usable.
239
     * This is a safety measure to prevent accessing methods of a destroyed instance.
240
     * @throws {Error} If the instance has been destroyed.
241
     */
242
    private ensureNotDestroyed(): void{
243
        if (this.isDestroyed()) throw new Error("Accessing to a method of a destroyed bs-darkmode-toggle instance.");
56✔
244
    }
245
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc