Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/application/application-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { ApplicationEventData, CssChangedEventData, DiscardedErrorEventData
import { applyAccessibilityCssToRoot, readyInitAccessibilityCssHelper, readyInitFontScale } from '../accessibility/accessibility-common';
import { getActiveWindow, getAppMainEntry, getAutoSystemAppearanceChanged, isAppInBackground, setActiveWindow, setAppInBackground, setAppMainEntry, setAutoSystemAppearanceChanged } from './helpers-common';
import { getNativeScriptGlobals } from '../globals/global-utils';
import { invalidateMediaQueryCache } from '../media-query-list';
import { SDK_VERSION } from '../utils/constants';
import type { NativeWindow, NativeWindowEventData, PrimaryWindowChangedEventData, WindowBase, WindowCloseEventData, WindowContentRequest, WindowContentResolver, WindowLayoutDirectionChangedEventData, WindowOpenEventData, WindowOpenOptions, WindowOrientationChangedEventData, WindowRole, WindowSystemAppearanceChangedEventData } from '../native-window';
import { NativeWindowEvents, WindowEvents } from '../native-window/native-window-interfaces';
Expand Down Expand Up @@ -670,6 +671,7 @@ export class ApplicationCommon {
* the change events, because app code observed the outgoing window's values.
*/
private syncTraitsFromWindow(nativeWindow: NativeWindow): void {
invalidateMediaQueryCache();
const orientation = nativeWindow.orientation();
if (orientation) {
if (this._orientation === undefined) {
Expand Down Expand Up @@ -970,6 +972,7 @@ export class ApplicationCommon {
}

this._orientation = value;
invalidateMediaQueryCache();

// Update metrics early enough regardless of the existence of root view
// Also, CSS will use the correct size values during update trigger
Expand Down Expand Up @@ -1030,6 +1033,7 @@ export class ApplicationCommon {
return;
}
this._systemAppearance = value;
invalidateMediaQueryCache();
this.systemAppearanceChanged(this.getRootView(), value);
this.notify(<SystemAppearanceChangedEventData>{
eventName: this.systemAppearanceChangedEvent,
Expand Down
39 changes: 38 additions & 1 deletion packages/core/media-query-list/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { matchMedia, checkIfMediaQueryMatches, MediaQueryList } from '.';
import { matchMedia, checkIfMediaQueryMatches, checkIfMediaQueryMatchesCached, invalidateMediaQueryCache, MediaQueryList } from '.';
import { Screen } from '../platform';

describe('media-query-list', () => {
Expand All @@ -14,6 +14,43 @@ describe('media-query-list', () => {
});
});

describe('checkIfMediaQueryMatchesCached', () => {
const query = `only screen and (max-width: ${widthDIPs})`;

afterEach(() => {
vi.restoreAllMocks();
});

it('evaluates each query once until invalidated', async () => {
await Promise.resolve();
invalidateMediaQueryCache();
const evaluate = vi.spyOn(Screen.mainScreen, 'heightPixels', 'get');

expect(checkIfMediaQueryMatchesCached(query)).toBe(true);
const afterFirst = evaluate.mock.calls.length;
expect(checkIfMediaQueryMatchesCached(query)).toBe(true);
expect(checkIfMediaQueryMatchesCached(query)).toBe(true);
expect(evaluate).toHaveBeenCalledTimes(afterFirst);

invalidateMediaQueryCache();
expect(checkIfMediaQueryMatchesCached(query)).toBe(true);
expect(evaluate.mock.calls.length).toBeGreaterThan(afterFirst);
});

it('re-evaluates after the screen size changes without an orientation change', async () => {
expect(checkIfMediaQueryMatchesCached(query)).toBe(true);
await Promise.resolve();

const { widthPixels } = Screen.mainScreen;
const width = vi.spyOn(Screen.mainScreen, 'widthPixels', 'get').mockReturnValue(widthPixels * 2);
expect(checkIfMediaQueryMatchesCached(query)).toBe(false);
width.mockRestore();

await Promise.resolve();
expect(checkIfMediaQueryMatchesCached(query)).toBe(true);
});
});

describe('matchMedia', () => {
it('should return a MediaQueryList that matches the css media query', () => {
const matchMediaWrapper = () => matchMedia(`only screen and (max-width: ${widthDIPs})`);
Expand Down
45 changes: 44 additions & 1 deletion packages/core/media-query-list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,49 @@ function checkIfMediaQueryMatches(mediaQueryString: string): boolean {
return matches;
}

// CSS matching asks the same queries for every view. Orientation and appearance changes go
// through invalidateMediaQueryCache; the screen size can change without either (a foldable's
// display, a window moved to another screen), so it is compared once per turn instead.
const cssQueryResults = new Map<string, boolean>();
let mediaQueryEpoch = 0;
let cachedScreenWidth: number;
let cachedScreenHeight: number;
let screenCheckedThisTurn = false;

function invalidateIfScreenSizeChanged(): void {
if (screenCheckedThisTurn) {
return;
}
screenCheckedThisTurn = true;
queueMicrotask(() => (screenCheckedThisTurn = false));

const { widthPixels, heightPixels } = Screen.mainScreen;
if (widthPixels !== cachedScreenWidth || heightPixels !== cachedScreenHeight) {
cachedScreenWidth = widthPixels;
cachedScreenHeight = heightPixels;
invalidateMediaQueryCache();
}
}

function checkIfMediaQueryMatchesCached(mediaQueryString: string): boolean {
invalidateIfScreenSizeChanged();
let matches = cssQueryResults.get(mediaQueryString);
if (matches === undefined) {
matches = checkIfMediaQueryMatches(mediaQueryString);
cssQueryResults.set(mediaQueryString, matches);
}
return matches;
}

function invalidateMediaQueryCache(): void {
cssQueryResults.clear();
mediaQueryEpoch++;
}

function getMediaQueryEpoch(): number {
return mediaQueryEpoch;
}

function matchMedia(mediaQueryString: string): MediaQueryListImpl {
isMediaInitializationEnabled = true;
const mediaQueryList = new MediaQueryListImpl();
Expand Down Expand Up @@ -202,4 +245,4 @@ class MediaQueryListImpl extends Observable implements MediaQueryList {
}
}

export { matchMedia, MediaQueryListImpl as MediaQueryList, checkIfMediaQueryMatches };
export { matchMedia, MediaQueryListImpl as MediaQueryList, checkIfMediaQueryMatches, checkIfMediaQueryMatchesCached, invalidateMediaQueryCache, getMediaQueryEpoch };
6 changes: 3 additions & 3 deletions packages/core/ui/styling/css-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isNullOrUndefined } from '../../utils/types';
import { cleanupImportantFlags } from './css-utils';

import * as ReworkCSS from '../../css';
import { checkIfMediaQueryMatches } from '../../media-query-list';
import { checkIfMediaQueryMatchesCached } from '../../media-query-list';

/**
* An interface describing the shape of a type on which the selectors may apply.
Expand Down Expand Up @@ -1127,7 +1127,7 @@ export function matchMediaQueryString(mediaQueryString: string | string[], cache
return true;
}

const result = checkIfMediaQueryMatches(mediaQueryString);
const result = checkIfMediaQueryMatchesCached(mediaQueryString);
if (result) {
cachedQueries.push(mediaQueryString);
return result;
Expand All @@ -1144,7 +1144,7 @@ export function matchMediaQueryString(mediaQueryString: string | string[], cache
continue;
}

if (!checkIfMediaQueryMatches(mq)) {
if (!checkIfMediaQueryMatchesCached(mq)) {
return false;
}

Expand Down
Loading