refactor(theme): remove obsolete compatibility
This commit is contained in:
1 parent
c19b62ff85
commit
dfb25b249b
3 files changed
+55
-137
No files matched your search
@@ -6,47 +6,36 @@ import { vi } from 'vitest';
|
|||||||
class ControllableMatchMedia {
|
class ControllableMatchMedia {
|
||||||
matches: boolean;
|
matches: boolean;
|
||||||
readonly media = '(prefers-color-scheme: light)';
|
readonly media = '(prefers-color-scheme: light)';
|
||||||
onchange: ((this: MediaQueryList, event: MediaQueryListEvent) => unknown) | null = null;
|
private listener?: (event: MediaQueryListEvent) => void;
|
||||||
private readonly listeners = new Set<(event: MediaQueryListEvent) => void>();
|
|
||||||
|
|
||||||
constructor(matches: boolean) {
|
constructor(matches: boolean) {
|
||||||
this.matches = matches;
|
this.matches = matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(_type: string, listener: EventListenerOrEventListenerObject | null): void {
|
addEventListener(_type: string, listener: (event: MediaQueryListEvent) => void): void {
|
||||||
if (typeof listener === 'function') {
|
this.listener = listener;
|
||||||
this.listeners.add(listener as (event: MediaQueryListEvent) => void);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeEventListener(_type: string, listener: EventListenerOrEventListenerObject | null): void {
|
|
||||||
if (typeof listener === 'function') {
|
|
||||||
this.listeners.delete(listener as (event: MediaQueryListEvent) => void);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener(listener: (this: MediaQueryList, event: MediaQueryListEvent) => unknown): void {
|
|
||||||
this.listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeListener(listener: (this: MediaQueryList, event: MediaQueryListEvent) => unknown): void {
|
|
||||||
this.listeners.delete(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchEvent(_event: Event): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
change(matches: boolean): void {
|
change(matches: boolean): void {
|
||||||
this.matches = matches;
|
this.matches = matches;
|
||||||
const event = { matches, media: this.media } as MediaQueryListEvent;
|
this.listener?.({ matches, media: this.media } as MediaQueryListEvent);
|
||||||
this.listeners.forEach((listener) => listener(event));
|
|
||||||
this.onchange?.call(this as unknown as MediaQueryList, event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectTheme(
|
||||||
|
button: HTMLButtonElement,
|
||||||
|
icon: string,
|
||||||
|
description: string,
|
||||||
|
isLight: boolean,
|
||||||
|
): void {
|
||||||
|
expect(button.querySelector('mat-icon')?.getAttribute('data-mat-icon-name')).toBe(icon);
|
||||||
|
expect(button.getAttribute('aria-label')).toBe(description);
|
||||||
|
expect(button.getAttribute('title')).toBe(description);
|
||||||
|
expect(document.body.classList.contains('light-mode')).toBe(isLight);
|
||||||
|
expect(document.body.classList.contains('dark-mode')).toBe(!isLight);
|
||||||
|
}
|
||||||
|
|
||||||
describe('DarkLightModeButton', () => {
|
describe('DarkLightModeButton', () => {
|
||||||
let component: DarkLightModeButton;
|
|
||||||
let fixture: ComponentFixture<DarkLightModeButton>;
|
let fixture: ComponentFixture<DarkLightModeButton>;
|
||||||
let mediaQuery: ControllableMatchMedia;
|
let mediaQuery: ControllableMatchMedia;
|
||||||
|
|
||||||
@@ -60,7 +49,6 @@ describe('DarkLightModeButton', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(DarkLightModeButton);
|
fixture = TestBed.createComponent(DarkLightModeButton);
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,23 +57,14 @@ describe('DarkLightModeButton', () => {
|
|||||||
document.body.classList.remove('light-mode', 'dark-mode');
|
document.body.classList.remove('light-mode', 'dark-mode');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('starts in System and resolves its initial appearance from the browser preference', () => {
|
it('starts in System and resolves its initial appearance from the browser preference', () => {
|
||||||
const button = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
|
const button = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
|
||||||
const icon = button.querySelector('mat-icon');
|
expectTheme(
|
||||||
|
button,
|
||||||
expect(icon?.getAttribute('data-mat-icon-name')).toBe('brightness_auto');
|
'brightness_auto',
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
|
||||||
'Theme: System (currently Light). Switch to Light theme',
|
'Theme: System (currently Light). Switch to Light theme',
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
expect(button.getAttribute('title')).toBe(
|
|
||||||
'Theme: System (currently Light). Switch to Light theme',
|
|
||||||
);
|
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(true);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cycles System to Light to Dark and wraps back to System', () => {
|
it('cycles System to Light to Dark and wraps back to System', () => {
|
||||||
@@ -93,45 +72,30 @@ describe('DarkLightModeButton', () => {
|
|||||||
|
|
||||||
button.click();
|
button.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(button.querySelector('mat-icon')?.getAttribute('data-mat-icon-name')).toBe(
|
expectTheme(
|
||||||
|
button,
|
||||||
'light_mode',
|
'light_mode',
|
||||||
);
|
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
|
||||||
'Theme: Light (currently Light). Switch to Dark theme',
|
'Theme: Light (currently Light). Switch to Dark theme',
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
expect(button.getAttribute('title')).toBe(
|
|
||||||
'Theme: Light (currently Light). Switch to Dark theme',
|
|
||||||
);
|
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(true);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(false);
|
|
||||||
|
|
||||||
button.click();
|
button.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(button.querySelector('mat-icon')?.getAttribute('data-mat-icon-name')).toBe(
|
expectTheme(
|
||||||
|
button,
|
||||||
'dark_mode',
|
'dark_mode',
|
||||||
);
|
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
|
||||||
'Theme: Dark (currently Dark). Switch to System theme',
|
'Theme: Dark (currently Dark). Switch to System theme',
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
expect(button.getAttribute('title')).toBe(
|
|
||||||
'Theme: Dark (currently Dark). Switch to System theme',
|
|
||||||
);
|
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(false);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(true);
|
|
||||||
|
|
||||||
button.click();
|
button.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(button.querySelector('mat-icon')?.getAttribute('data-mat-icon-name')).toBe(
|
expectTheme(
|
||||||
|
button,
|
||||||
'brightness_auto',
|
'brightness_auto',
|
||||||
);
|
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
|
||||||
'Theme: System (currently Light). Switch to Light theme',
|
'Theme: System (currently Light). Switch to Light theme',
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
expect(button.getAttribute('title')).toBe(
|
|
||||||
'Theme: System (currently Light). Switch to Light theme',
|
|
||||||
);
|
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(true);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('follows live browser changes while System is selected', () => {
|
it('follows live browser changes while System is selected', () => {
|
||||||
@@ -139,22 +103,21 @@ describe('DarkLightModeButton', () => {
|
|||||||
|
|
||||||
mediaQuery.change(false);
|
mediaQuery.change(false);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
expectTheme(
|
||||||
|
button,
|
||||||
|
'brightness_auto',
|
||||||
'Theme: System (currently Dark). Switch to Light theme',
|
'Theme: System (currently Dark). Switch to Light theme',
|
||||||
);
|
false,
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(false);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(true);
|
|
||||||
expect(document.body.classList.contains('light-mode')).not.toBe(
|
|
||||||
document.body.classList.contains('dark-mode'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
mediaQuery.change(true);
|
mediaQuery.change(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
expectTheme(
|
||||||
|
button,
|
||||||
|
'brightness_auto',
|
||||||
'Theme: System (currently Light). Switch to Light theme',
|
'Theme: System (currently Light). Switch to Light theme',
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(true);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps explicit modes fixed and uses the latest browser value when returning to System', () => {
|
it('keeps explicit modes fixed and uses the latest browser value when returning to System', () => {
|
||||||
@@ -164,29 +127,32 @@ describe('DarkLightModeButton', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
mediaQuery.change(false);
|
mediaQuery.change(false);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
expectTheme(
|
||||||
|
button,
|
||||||
|
'light_mode',
|
||||||
'Theme: Light (currently Light). Switch to Dark theme',
|
'Theme: Light (currently Light). Switch to Dark theme',
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(true);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(false);
|
|
||||||
|
|
||||||
button.click();
|
button.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
mediaQuery.change(true);
|
mediaQuery.change(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
expectTheme(
|
||||||
|
button,
|
||||||
|
'dark_mode',
|
||||||
'Theme: Dark (currently Dark). Switch to System theme',
|
'Theme: Dark (currently Dark). Switch to System theme',
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(false);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(true);
|
|
||||||
|
|
||||||
mediaQuery.change(false);
|
mediaQuery.change(false);
|
||||||
button.click();
|
button.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(button.getAttribute('aria-label')).toBe(
|
expectTheme(
|
||||||
|
button,
|
||||||
|
'brightness_auto',
|
||||||
'Theme: System (currently Dark). Switch to Light theme',
|
'Theme: System (currently Dark). Switch to Light theme',
|
||||||
|
false,
|
||||||
);
|
);
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(false);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { provideZonelessChangeDetection } from '@angular/core';
|
|
||||||
import { ThemeService } from './theme-service';
|
|
||||||
|
|
||||||
describe('ThemeService', () => {
|
|
||||||
let service: ThemeService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
providers: [provideZonelessChangeDetection()],
|
|
||||||
});
|
|
||||||
service = TestBed.inject(ThemeService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be created', () => {
|
|
||||||
expect(service).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialize with system preference', () => {
|
|
||||||
expect(service.isLightMode()).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should toggle theme', () => {
|
|
||||||
const initialMode = service.isLightMode();
|
|
||||||
service.toggleTheme();
|
|
||||||
expect(service.isLightMode()).toBe(!initialMode);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should force light mode when specified', () => {
|
|
||||||
service.toggleTheme(true);
|
|
||||||
expect(service.isLightMode()).toBe(true);
|
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(true);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should force dark mode when specified', () => {
|
|
||||||
service.toggleTheme(false);
|
|
||||||
expect(service.isLightMode()).toBe(false);
|
|
||||||
expect(document.body.classList.contains('light-mode')).toBe(false);
|
|
||||||
expect(document.body.classList.contains('dark-mode')).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -13,7 +13,6 @@ export class ThemeService {
|
|||||||
const preference = this.selectedPreference();
|
const preference = this.selectedPreference();
|
||||||
return preference === 'system' ? (this.systemIsLight() ? 'light' : 'dark') : preference;
|
return preference === 'system' ? (this.systemIsLight() ? 'light' : 'dark') : preference;
|
||||||
});
|
});
|
||||||
readonly isLightMode = computed(() => this.effectiveTheme() === 'light');
|
|
||||||
readonly nextThemePreference = computed(() => this.nextPreference(this.selectedPreference()));
|
readonly nextThemePreference = computed(() => this.nextPreference(this.selectedPreference()));
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -26,20 +25,15 @@ export class ThemeService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setThemePreference(preference: ThemePreference): void {
|
toggleTheme(): void {
|
||||||
|
this.setThemePreference(this.nextThemePreference());
|
||||||
|
}
|
||||||
|
|
||||||
|
private setThemePreference(preference: ThemePreference): void {
|
||||||
this.selectedPreference.set(preference);
|
this.selectedPreference.set(preference);
|
||||||
this.applyEffectiveTheme();
|
this.applyEffectiveTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTheme(forceLight?: boolean): void {
|
|
||||||
if (forceLight !== undefined) {
|
|
||||||
this.setThemePreference(forceLight ? 'light' : 'dark');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setThemePreference(this.nextThemePreference());
|
|
||||||
}
|
|
||||||
|
|
||||||
private nextPreference(preference: ThemePreference): ThemePreference {
|
private nextPreference(preference: ThemePreference): ThemePreference {
|
||||||
return preference === 'system' ? 'light' : preference === 'light' ? 'dark' : 'system';
|
return preference === 'system' ? 'light' : preference === 'light' ? 'dark' : 'system';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user