Skip to content

Commit

Permalink
fix(useAutoDetectAppearance): rm window from useState (#6243)
Browse files Browse the repository at this point in the history
Не учёл, что `useState()` вызывается и на сервере. Первое, что может придти в голову – это проверять в `useState` существует ли `window`, но так сделать не можем, т.к. на клиенте, если пользователь не передал `appearance`, то условие отработает удовлетворительно и в первый мы получим автоматическое определение `appearance`, тем самым у нас уже будет разница между клиентом и сервером.

В `useState` теперь возвращаем либо пользовательский `appearance`, либо `'light'` по умолчанию.

В самом эффекте добавил условие для `window`, чтобы убрать неочевидный **Non-null Assertion Operator** (`!`).
  • Loading branch information
inomdzhon authored Dec 11, 2023
1 parent 7a838ab commit bf6169d
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 10 deletions.
89 changes: 89 additions & 0 deletions packages/vkui/src/hooks/useAutoDetectAppearance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { act, renderHook } from '@testing-library/react';
import { noop } from '@vkontakte/vkjs';
import { Appearance } from '../lib/appearance';
import * as LibDOM from '../lib/dom';
import { useAutoDetectAppearance } from './useAutoDetectAppearance';

jest.mock('../lib/dom', () => {
return {
__esModule: true,
...jest.requireActual('../lib/dom'),
};
});

describe(useAutoDetectAppearance, () => {
describe('client', () => {
it.each([Appearance.LIGHT, Appearance.DARK])(
'should return appearance by property (%s)',
(appearanceProp) => {
const { result } = renderHook(() => useAutoDetectAppearance(appearanceProp));
expect(result.current).toBe(appearanceProp);
},
);

it.each([
{
initialMatches: false,
listenerMatches: false,
appearance: { before: Appearance.LIGHT, after: Appearance.LIGHT },
},
{
initialMatches: true,
listenerMatches: true,
appearance: { before: Appearance.DARK, after: Appearance.DARK },
},
{
initialMatches: false,
listenerMatches: true,
appearance: { before: Appearance.LIGHT, after: Appearance.DARK },
},
{
initialMatches: true,
listenerMatches: false,
appearance: { before: Appearance.DARK, after: Appearance.LIGHT },
},
])(
'should auto detect appearance (initialMatches is $initialMatches, listenerMatches is $listenerMatches, appearance is $appearance)',
({ initialMatches, listenerMatches, appearance }) => {
let addEventListenerHandler = noop;
const addEventListener = jest.fn().mockImplementation((_, handlerByHook) => {
addEventListenerHandler = () => {
handlerByHook({ matches: listenerMatches });
};
});

// Объявление скопировано из документации https://jestjs.io/docs/manual-mocks
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: initialMatches,
media: query,
onchange: null,
addListener: addEventListener, // устарело
removeListener: jest.fn(), // устарело
addEventListener: addEventListener,
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
const { result } = renderHook(() => useAutoDetectAppearance());
expect(result.current).toBe(appearance.before);
act(addEventListenerHandler);
expect(result.current).toBe(appearance.after);
},
);
});

describe('server', () => {
it('should auto detect appearance ($appearance)', () => {
jest.spyOn<any, any>(LibDOM, 'useDOM').mockReturnValue(() => {
return {
document: undefined,
window: undefined,
};
});
const { result } = renderHook(() => useAutoDetectAppearance());
expect(result.current).toBe(Appearance.LIGHT);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { noop } from '@vkontakte/vkjs';
import type { AppearanceType } from '../lib/appearance';
import { Appearance, type AppearanceType } from '../lib/appearance';
import { useDOM } from '../lib/dom';
import { matchMediaListAddListener, matchMediaListRemoveListener } from '../lib/matchMedia';
import { useIsomorphicLayoutEffect } from '../lib/useIsomorphicLayoutEffect';
Expand All @@ -11,29 +11,25 @@ import { useIsomorphicLayoutEffect } from '../lib/useIsomorphicLayoutEffect';
export const useAutoDetectAppearance = (appearanceProp?: AppearanceType): AppearanceType => {
const { window } = useDOM();

const [appearance, setAppearance] = React.useState<AppearanceType>(() => {
if (appearanceProp) {
return appearanceProp;
}
// eslint-disable-next-line no-restricted-properties
return window!.matchMedia('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light';
});
const [appearance, setAppearance] = React.useState<AppearanceType>(
appearanceProp || Appearance.LIGHT,
);

useIsomorphicLayoutEffect(() => {
if (appearanceProp) {
setAppearance(appearanceProp);
return noop;
}

const mediaQuery = window!.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window ? window.matchMedia('(prefers-color-scheme: dark)') : undefined;

if (!mediaQuery) {
return noop;
}

const check = (event: MediaQueryList | MediaQueryListEvent) => {
// eslint-disable-next-line no-restricted-properties
setAppearance(event.matches ? 'dark' : 'light');
setAppearance(event.matches ? Appearance.DARK : Appearance.LIGHT);
};
check(mediaQuery);
matchMediaListAddListener(mediaQuery, check);
Expand Down

0 comments on commit bf6169d

Please sign in to comment.