Skip to content
2 changes: 1 addition & 1 deletion core/src/components/col/col.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Listen, Prop, forceUpdate, h } from '@stencil/core';
import { matchBreakpoint } from '@utils/breakpoints';
import { printIonWarning } from '@utils/logging';
import { matchBreakpoint } from '@utils/media';

import type { IonColProperty, IonColStyle } from './col.interface';
import { ION_COL_BREAKPOINTS } from './col.interface';
Expand Down
29 changes: 11 additions & 18 deletions core/src/components/grid/test/fixed/grid.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
import { expect } from '@playwright/test';
import type { ScreenBreakpoint } from '@utils/breakpoints';
import { DEFAULT_SCREEN_BREAKPOINTS, SCREEN_BREAKPOINT_NAMES } from '@utils/breakpoints';
import { configs, test } from '@utils/test/playwright';

import { defaultTheme as mdTheme } from '../../../../themes/md/default.tokens';
import { SIZE_TO_MEDIA } from '../../../../utils/media';
import { ION_GRID_BREAKPOINTS } from '../../grid.interface';

const ionGridBreakpoints = mdTheme.components!.IonGrid!.breakpoint!;

/**
* Parse the min-width (in px) out of each `SIZE_TO_MEDIA` entry. These are
* the same activation thresholds `matchBreakpoint` evaluates at runtime and
* that `$screen-breakpoints` defines on the SCSS side.
*/
const minWidthFor = (bp: (typeof ION_GRID_BREAKPOINTS)[number]): number => {
const match = (SIZE_TO_MEDIA[bp] as string).match(/\(min-width:\s*(\d+)px\)/);
return match ? parseInt(match[1], 10) : 0;
};

/**
* Viewport width that activates each breakpoint. `max(400, minWidth)` lands
* exactly on the threshold for every breakpoint above xs (firing only that
* breakpoint's rule and nothing above it) while giving xs a renderable non-zero
* viewport.
* exactly on the threshold for sm and up (firing only that breakpoint's rule
* and nothing above it) while giving xs a renderable non-zero viewport.
*
* The default screen breakpoints are used rather than the resolved ones,
* since the test page does not override them and `$screen-breakpoints`
* defines the same values on the SCSS side.
*/
const VIEWPORT_AT_BREAKPOINT = Object.fromEntries(
ION_GRID_BREAKPOINTS.map((bp) => [bp, Math.max(400, minWidthFor(bp))])
) as Record<(typeof ION_GRID_BREAKPOINTS)[number], number>;
SCREEN_BREAKPOINT_NAMES.map((bp) => [bp, Math.max(400, DEFAULT_SCREEN_BREAKPOINTS[bp])])
) as Record<ScreenBreakpoint, number>;

/**
* This behavior does not vary across modes/directions.
Expand All @@ -36,7 +29,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await page.goto('/src/components/grid/test/fixed', config);
});

for (const breakpoint of ION_GRID_BREAKPOINTS) {
for (const breakpoint of SCREEN_BREAKPOINT_NAMES) {
test(`fixed grid matches the ${breakpoint} width token`, async ({ page }) => {
const viewportWidth = VIEWPORT_AT_BREAKPOINT[breakpoint];
await page.setViewportSize({ width: viewportWidth, height: 800 });
Expand Down
9 changes: 9 additions & 0 deletions core/src/global/ionic-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { applyComponentsTheme, applyGlobalTheme, getCustomTheme } from '@utils/t
import type { IonicConfig, Mode, Theme } from '../interface';
import { defaultTheme as baseTheme } from '../themes/base/default.tokens';
import type { DefaultTheme } from '../themes/themes.interfaces';
import { refreshBreakpointListeners } from '../utils/breakpoints';
import { shouldUseCloseWatcher } from '../utils/hardware-back-button';
import { isPlatform, setupPlatforms } from '../utils/platform';

Expand Down Expand Up @@ -120,6 +121,7 @@ export const initialize = (userConfig: IonicConfig = {}) => {
};

config.reset(configObj);

if (config.getBoolean('persistConfig')) {
saveConfig(win, configObj);
}
Expand Down Expand Up @@ -199,6 +201,13 @@ export const initialize = (userConfig: IonicConfig = {}) => {
config.set(configKey, value);
});

/**
* The breakpoint listeners are built from the configured widths, so they
* have to be rebuilt once the configuration is settled. This runs after the
* theme's config is merged, since a theme can set the widths itself.
*/
refreshBreakpointListeners();

if (config.getBoolean('_testing')) {
config.set('animated', false);
}
Expand Down
71 changes: 69 additions & 2 deletions core/src/global/test/ionic-global.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ jest.mock('@stencil/core', () => {
/**
* The implementation needs to be mocked before the implementation is imported.
*/
// eslint-disable-next-line import/first
import { getIonTheme, getIonMode } from '../ionic-global';
import { onBreakpointChange, resetBreakpointListeners, resetScreenBreakpoints } from '@utils/breakpoints';

import { config } from '../config';
import { getIonTheme, getIonMode, initialize } from '../ionic-global';

describe('Ionic Global', () => {
describe('getIonMode', () => {
Expand Down Expand Up @@ -107,4 +109,69 @@ describe('Ionic Global', () => {
expect(getIonTheme()).toBe('md');
});
});

describe('initialize', () => {
const originalMatchMedia = (window as any).matchMedia;

/** Every media query the breakpoint listeners have asked for, in order. */
let requested: string[];

beforeEach(() => {
requested = [];

(window as any).matchMedia = (media: string) => {
requested.push(media);

return { media, matches: false, addEventListener: () => {}, removeEventListener: () => {} };
};

config.set('screenBreakpoints', undefined as any);
resetScreenBreakpoints();
resetBreakpointListeners();
});

afterEach(() => {
resetBreakpointListeners();
resetScreenBreakpoints();
config.set('screenBreakpoints', undefined as any);
(window as any).matchMedia = originalMatchMedia;
});

/**
* A theme can set the screen breakpoints in its own config block, which is
* merged in after the application config, so the listeners can only be
* rebuilt once both have been applied.
*/
it('should rebuild the breakpoint listeners from a theme that sets the widths', () => {
// Subscribing first is what creates the listeners there are to rebuild.
const unsubscribe = onBreakpointChange(() => {});
requested.length = 0;

initialize({ customTheme: { config: { screenBreakpoints: { lg: 700 } } } } as any);

expect(requested).toContain('(min-width: 700px)');
expect(requested).not.toContain('(min-width: 992px)');

unsubscribe();
});

/**
* The theme's config is merged over the application's, so widths set in
* either are kept and the listeners are built from the result.
*/
it('should keep widths set by the application alongside those from the theme', () => {
const unsubscribe = onBreakpointChange(() => {});
requested.length = 0;

initialize({
screenBreakpoints: { sm: 300 },
customTheme: { config: { screenBreakpoints: { lg: 700 } } },
} as any);

expect(requested).toContain('(min-width: 300px)');
expect(requested).toContain('(min-width: 700px)');

unsubscribe();
});
});
});
Loading
Loading