fix(appearance): support MediaQueryList change events on legacy Safari
Safari <14 (macOS 10.13/10.14) and other legacy browsers do not implement
MediaQueryList.addEventListener — the property is undefined. initializeTheme()
runs at app boot and called mediaQuery()?.addEventListener('change', ...); the
optional chaining guarded a null query but not the missing method, so it threw
'addEventListener is not a function' during module evaluation and white-screened
the whole app for those users (PHP-LARAVEL-41).
Add addMediaQueryListener/removeMediaQueryListener helpers that fall back to the
deprecated addListener/removeListener when the modern API is absent, and route
the prefers-color-scheme (use-appearance) and mobile-breakpoint (use-mobile)
subscriptions through them. Both call sites had the same latent crash.
Fixes PHP-LARAVEL-41
This commit is contained in:
parent
05d4bae0af
commit
8df1990617
|
|
@ -1,3 +1,7 @@
|
|||
import {
|
||||
addMediaQueryListener,
|
||||
removeMediaQueryListener,
|
||||
} from '@/lib/media-query';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export type Appearance = 'light' | 'dark' | 'system';
|
||||
|
|
@ -53,7 +57,10 @@ export function initializeTheme() {
|
|||
applyTheme(savedAppearance);
|
||||
|
||||
// Add the event listener for system theme changes...
|
||||
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
|
||||
const mql = mediaQuery();
|
||||
if (mql) {
|
||||
addMediaQueryListener(mql, handleSystemThemeChange);
|
||||
}
|
||||
}
|
||||
|
||||
export function useAppearance() {
|
||||
|
|
@ -78,11 +85,12 @@ export function useAppearance() {
|
|||
|
||||
updateAppearance(savedAppearance || 'system');
|
||||
|
||||
return () =>
|
||||
mediaQuery()?.removeEventListener(
|
||||
'change',
|
||||
handleSystemThemeChange,
|
||||
);
|
||||
return () => {
|
||||
const mql = mediaQuery();
|
||||
if (mql) {
|
||||
removeMediaQueryListener(mql, handleSystemThemeChange);
|
||||
}
|
||||
};
|
||||
}, [updateAppearance]);
|
||||
|
||||
return { appearance, updateAppearance } as const;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import {
|
||||
addMediaQueryListener,
|
||||
removeMediaQueryListener,
|
||||
} from '@/lib/media-query';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
|
@ -11,8 +15,8 @@ function subscribe(callback: () => void) {
|
|||
const mql = getMediaQueryList();
|
||||
if (!mql) return () => {};
|
||||
|
||||
mql.addEventListener('change', callback);
|
||||
return () => mql.removeEventListener('change', callback);
|
||||
addMediaQueryListener(mql, callback);
|
||||
return () => removeMediaQueryListener(mql, callback);
|
||||
}
|
||||
|
||||
function getSnapshot() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { addMediaQueryListener, removeMediaQueryListener } from './media-query';
|
||||
|
||||
function fakeMql(overrides: Partial<MediaQueryList>): MediaQueryList {
|
||||
return { matches: false, media: '', ...overrides } as MediaQueryList;
|
||||
}
|
||||
|
||||
describe('addMediaQueryListener', () => {
|
||||
it('uses the modern addEventListener when available', () => {
|
||||
const addEventListener = vi.fn();
|
||||
const handler = () => {};
|
||||
|
||||
addMediaQueryListener(fakeMql({ addEventListener }), handler);
|
||||
|
||||
expect(addEventListener).toHaveBeenCalledWith('change', handler);
|
||||
});
|
||||
|
||||
it('falls back to the deprecated addListener on Safari <14', () => {
|
||||
const addListener = vi.fn();
|
||||
const handler = () => {};
|
||||
|
||||
// addEventListener undefined mirrors old Safari, where the modern API is
|
||||
// missing and the previous code threw at boot.
|
||||
addMediaQueryListener(
|
||||
fakeMql({ addEventListener: undefined, addListener }),
|
||||
handler,
|
||||
);
|
||||
|
||||
expect(addListener).toHaveBeenCalledWith(handler);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMediaQueryListener', () => {
|
||||
it('uses the modern removeEventListener when available', () => {
|
||||
const removeEventListener = vi.fn();
|
||||
const handler = () => {};
|
||||
|
||||
removeMediaQueryListener(fakeMql({ removeEventListener }), handler);
|
||||
|
||||
expect(removeEventListener).toHaveBeenCalledWith('change', handler);
|
||||
});
|
||||
|
||||
it('falls back to the deprecated removeListener on Safari <14', () => {
|
||||
const removeListener = vi.fn();
|
||||
const handler = () => {};
|
||||
|
||||
removeMediaQueryListener(
|
||||
fakeMql({ removeEventListener: undefined, removeListener }),
|
||||
handler,
|
||||
);
|
||||
|
||||
expect(removeListener).toHaveBeenCalledWith(handler);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Cross-browser MediaQueryList change subscription.
|
||||
*
|
||||
* Safari <14 and other legacy browsers do not implement
|
||||
* MediaQueryList.addEventListener / removeEventListener — the property is
|
||||
* undefined, so calling it throws "addEventListener is not a function". At app
|
||||
* boot (initializeTheme) that aborts the whole mount and white-screens the app
|
||||
* for those users. Such browsers only expose the deprecated addListener /
|
||||
* removeListener, so fall back to them when the modern API is missing.
|
||||
*/
|
||||
export function addMediaQueryListener(
|
||||
mql: MediaQueryList,
|
||||
handler: () => void,
|
||||
): void {
|
||||
if (typeof mql.addEventListener === 'function') {
|
||||
mql.addEventListener('change', handler);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
mql.addListener(handler);
|
||||
}
|
||||
|
||||
export function removeMediaQueryListener(
|
||||
mql: MediaQueryList,
|
||||
handler: () => void,
|
||||
): void {
|
||||
if (typeof mql.removeEventListener === 'function') {
|
||||
mql.removeEventListener('change', handler);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
mql.removeListener(handler);
|
||||
}
|
||||
Loading…
Reference in New Issue