diff --git a/BREAKING.md b/BREAKING.md index 6c022d272dd..a4eeae89385 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -34,6 +34,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver - [Row](#version-10x-row) - [Skeleton Text](#version-10x-skeleton-text) - [Spinner](#version-10x-spinner) + - [Split Pane](#version-10x-split-pane) - [Text](#version-10x-text) - [Textarea](#version-10x-textarea) - [Thumbnail](#version-10x-thumbnail) @@ -496,6 +497,10 @@ Remove any instances that target the theme classes: `ion-skeleton-text.md`, `ion - `.spinner-[spinner-name]` → `.spinner-name-[spinner-name]` - Specific theme classes (e.g., `ion-spinner.md`) are no longer supported. Style modifications based on the active theme must be implemented using theme tokens rather than direct class targeting. +

Split Pane

+ +- The default value of the `when` property changed from `'(min-width: 992px)'` to the equivalent `'lg'` shortcut. The default behavior is unchanged, but the shortcut now resolves through the global `screenBreakpoints` config, so overriding `lg` also changes when the split pane becomes visible. Code that compares `when` against the literal `'(min-width: 992px)'` should be updated. +

Text

The following breaking changes apply to `ion-text`: diff --git a/core/api.txt b/core/api.txt index 88d97384026..f70e4bb3493 100644 --- a/core/api.txt +++ b/core/api.txt @@ -2605,7 +2605,7 @@ ion-split-pane,prop,contentId,string | undefined,undefined,false,true ion-split-pane,prop,disabled,boolean,false,false,false ion-split-pane,prop,mode,"ios" | "md",undefined,false,false ion-split-pane,prop,theme,"ios" | "md" | "ionic",undefined,false,false -ion-split-pane,prop,when,boolean | string,'(min-width: 992px)',false,false +ion-split-pane,prop,when,boolean | string,'lg',false,false ion-split-pane,event,ionSplitPaneVisible,{ visible: boolean; },true ion-split-pane,css-prop,--border,ionic ion-split-pane,css-prop,--border,ios diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ad3d00ea232..a4ef78faace 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3878,8 +3878,8 @@ export namespace Components { */ "theme"?: "ios" | "md" | "ionic"; /** - * When the split-pane should be shown. Can be a CSS media query expression, or a shortcut expression. Can also be a boolean expression. - * @default '(min-width: 992px)' + * When the split-pane should be shown. Can be a CSS media query expression, or a shortcut expression. Can also be a boolean expression. The shortcut expressions are the names of the global screen breakpoints (`"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"` and `"xxl"`), which expand to the `min-width` media query for that breakpoint, plus `"never"`, which keeps the split pane hidden at every size. The width each breakpoint activates at can be changed with the `screenBreakpoints` config. + * @default 'lg' */ "when": string | boolean; } @@ -9848,8 +9848,8 @@ declare namespace LocalJSX { */ "theme"?: "ios" | "md" | "ionic"; /** - * When the split-pane should be shown. Can be a CSS media query expression, or a shortcut expression. Can also be a boolean expression. - * @default '(min-width: 992px)' + * When the split-pane should be shown. Can be a CSS media query expression, or a shortcut expression. Can also be a boolean expression. The shortcut expressions are the names of the global screen breakpoints (`"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"` and `"xxl"`), which expand to the `min-width` media query for that breakpoint, plus `"never"`, which keeps the split pane hidden at every size. The width each breakpoint activates at can be changed with the `screenBreakpoints` config. + * @default 'lg' */ "when"?: string | boolean; } diff --git a/core/src/components/split-pane/split-pane.tsx b/core/src/components/split-pane/split-pane.tsx index 95c8995f8ef..d12d44c6232 100644 --- a/core/src/components/split-pane/split-pane.tsx +++ b/core/src/components/split-pane/split-pane.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core'; +import { getScreenBreakpointMediaQuery } from '@utils/breakpoints'; import { printIonWarning } from '@utils/logging'; import { getIonTheme } from '../../global/ionic-global'; @@ -8,14 +9,29 @@ import { getIonTheme } from '../../global/ionic-global'; const SPLIT_PANE_MAIN = 'split-pane-main'; const SPLIT_PANE_SIDE = 'split-pane-side'; -// TODO(FW-7285): Replace with global breakpoints -const QUERY: { [key: string]: string } = { - xs: '(min-width: 0px)', - sm: '(min-width: 576px)', - md: '(min-width: 768px)', - lg: '(min-width: 992px)', - xl: '(min-width: 1200px)', - never: '', + +/** + * The shortcut expression that keeps the split pane hidden at every size. + */ +const NEVER = 'never'; + +/** + * Resolve the `when` property to the media query to listen on. A shortcut + * expression is expanded to the `min-width` query of the matching global + * screen breakpoint, so it reflects any `screenBreakpoints` config the + * application has set. Anything else is treated as a media query and used + * as-is. + * + * @param when The `when` property value. + * @return The media query to listen on, or an empty string when the split + * pane should never be shown. + */ +const getMediaQuery = (when: string): string => { + if (when === NEVER) { + return ''; + } + + return getScreenBreakpointMediaQuery(when) ?? when; }; /** @@ -55,8 +71,14 @@ export class SplitPane implements ComponentInterface { * When the split-pane should be shown. * Can be a CSS media query expression, or a shortcut expression. * Can also be a boolean expression. + * + * The shortcut expressions are the names of the global screen breakpoints + * (`"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"` and `"xxl"`), which expand to the + * `min-width` media query for that breakpoint, plus `"never"`, which keeps + * the split pane hidden at every size. The width each breakpoint activates + * at can be changed with the `screenBreakpoints` config. */ - @Prop() when: string | boolean = QUERY['lg']; + @Prop() when: string | boolean = 'lg'; /** * Expression to be called when the split-pane visibility has changed @@ -118,7 +140,7 @@ export class SplitPane implements ComponentInterface { } // When query is a string, let's find first if it is a shortcut - const mediaQuery = QUERY[query] || query; + const mediaQuery = getMediaQuery(query); // Media query is empty or null, we hide it if (mediaQuery.length === 0) { diff --git a/core/src/components/split-pane/test/when/index.html b/core/src/components/split-pane/test/when/index.html new file mode 100644 index 00000000000..cccdacbbe2c --- /dev/null +++ b/core/src/components/split-pane/test/when/index.html @@ -0,0 +1,127 @@ + + + + + Split Pane - When + + + + + + + + + + + + + + + Menu + + +
+ + Main content. The split pane is shown at md and above, which this page configures as 400px. + +
+
+ +
+ … + –px +
+
+ + + + diff --git a/core/src/components/split-pane/test/when/split-pane.e2e.ts b/core/src/components/split-pane/test/when/split-pane.e2e.ts new file mode 100644 index 00000000000..2f93db1c4ed --- /dev/null +++ b/core/src/components/split-pane/test/when/split-pane.e2e.ts @@ -0,0 +1,188 @@ +import { expect } from '@playwright/test'; +import type { E2EPage } from '@utils/test/playwright'; +import { configs, test } from '@utils/test/playwright'; + +/** + * `when` accepts a raw CSS media query, one of the global screen breakpoint + * shortcuts, the `never` shortcut, or a boolean. + * + * Cases that set the markup inline default to an 800px viewport, which sits + * between the default `md` (768) and `lg` (992) breakpoints. The configured + * breakpoint cases load the test page instead, which moves `md` to 400px. + * + * This behavior does not vary across modes/directions. + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('split-pane: when'), () => { + const setUpSplitPane = async (page: E2EPage, when: string | undefined, width = 800) => { + // Omit the prop entirely when `when` is undefined rather than set + // it to "", so the prop keeps its default + const whenAttribute = when !== undefined ? `when="${when}"` : ''; + + await page.setViewportSize({ width, height: 600 }); + await page.setContent( + ` + + + + Menu + +
+ Main +
+
+
+ `, + config + ); + + return page.locator('ion-split-pane'); + }; + + /** + * The default is the `lg` shortcut rather than the `(min-width: 992px)` + * query it used to be. Both resolve to the same width by default, so + * these pin the shortcut itself as well as where it activates. + */ + test.describe('with no value', () => { + test('should default to the lg shortcut', async ({ page }) => { + const splitPane = await setUpSplitPane(page, undefined); + + expect(await splitPane.evaluate((el: any) => el.when)).toBe('lg'); + }); + + test('should not be visible below the default lg width', async ({ page }) => { + const splitPane = await setUpSplitPane(page, undefined, 900); + + await expect(splitPane).not.toHaveClass(/split-pane-visible/); + }); + + test('should be visible at the default lg width', async ({ page }) => { + const splitPane = await setUpSplitPane(page, undefined, 1000); + + await expect(splitPane).toHaveClass(/split-pane-visible/); + }); + }); + + test.describe('with a raw media query', () => { + test('should be visible when the query matches', async ({ page }) => { + const splitPane = await setUpSplitPane(page, '(min-width: 500px)'); + + await expect(splitPane).toHaveClass(/split-pane-visible/); + }); + + test('should not be visible when the query does not match', async ({ page }) => { + const splitPane = await setUpSplitPane(page, '(min-width: 900px)'); + + await expect(splitPane).not.toHaveClass(/split-pane-visible/); + }); + + test('should support a query that is not a min-width', async ({ page }) => { + const splitPane = await setUpSplitPane(page, '(orientation: landscape)'); + + await expect(splitPane).toHaveClass(/split-pane-visible/); + }); + }); + + test.describe('with a breakpoint shortcut', () => { + test('should be visible at or above the breakpoint', async ({ page }) => { + const splitPane = await setUpSplitPane(page, 'md'); + + await expect(splitPane).toHaveClass(/split-pane-visible/); + }); + + test('should not be visible below the breakpoint', async ({ page }) => { + const splitPane = await setUpSplitPane(page, 'lg'); + + await expect(splitPane).not.toHaveClass(/split-pane-visible/); + }); + + test('should support the xxl breakpoint', async ({ page }) => { + // Below the 1400px xxl breakpoint + const narrow = await setUpSplitPane(page, 'xxl', 1300); + await expect(narrow).not.toHaveClass(/split-pane-visible/); + + // At the xxl breakpoint + const wide = await setUpSplitPane(page, 'xxl', 1440); + await expect(wide).toHaveClass(/split-pane-visible/); + }); + }); + + test.describe('with the "never" shortcut', () => { + test('should not be visible on a narrow screen', async ({ page }) => { + const splitPane = await setUpSplitPane(page, 'never', 400); + + await expect(splitPane).not.toHaveClass(/split-pane-visible/); + }); + + test('should not be visible on a wide screen', async ({ page }) => { + const splitPane = await setUpSplitPane(page, 'never', 1600); + + await expect(splitPane).not.toHaveClass(/split-pane-visible/); + }); + }); + + test.describe('with an unrecognized value', () => { + test('should not be visible', async ({ page }) => { + const splitPane = await setUpSplitPane(page, 'not-a-breakpoint'); + + await expect(splitPane).not.toHaveClass(/split-pane-visible/); + }); + }); + + /** + * A boolean can only be set as a JavaScript property, since an HTML + * attribute is always a string. Each case starts from a shortcut that + * puts the split pane in the opposite state at the 800px viewport, so + * the assertion proves the boolean caused the change rather than the + * split pane already being in that state. + */ + test.describe('with a boolean', () => { + test('should be visible when true', async ({ page }) => { + // lg (992px) does not match at 800px, so this starts hidden + const splitPane = await setUpSplitPane(page, 'lg'); + + await expect(splitPane).not.toHaveClass(/split-pane-visible/); + + await splitPane.evaluate((el: any) => (el.when = true)); + await page.waitForChanges(); + + await expect(splitPane).toHaveClass(/split-pane-visible/); + }); + + test('should not be visible when false', async ({ page }) => { + // md (768px) matches at 800px, so this starts visible + const splitPane = await setUpSplitPane(page, 'md'); + + await expect(splitPane).toHaveClass(/split-pane-visible/); + + await splitPane.evaluate((el: any) => (el.when = false)); + await page.waitForChanges(); + + await expect(splitPane).not.toHaveClass(/split-pane-visible/); + }); + }); + + /** + * The shortcut expands to the `min-width` query of the matching global + * screen breakpoint, so moving that breakpoint moves when the split pane + * appears. The page sets `md` to 400px, well below its default of 768px. + */ + test.describe('with a configured screen breakpoint', () => { + test('should not be visible below the configured width', async ({ page }) => { + await page.setViewportSize({ width: 350, height: 600 }); + await page.goto('/src/components/split-pane/test/when', config); + + await expect(page.locator('#split-pane')).not.toHaveClass(/split-pane-visible/); + }); + + test('should be visible at the configured width', async ({ page }) => { + // 500px is above the configured md (400) but below the default md (768) + await page.setViewportSize({ width: 500, height: 600 }); + await page.goto('/src/components/split-pane/test/when', config); + + await expect(page.locator('#split-pane')).toHaveClass(/split-pane-visible/); + }); + }); + }); +});