From 5ac19327d4c88f42cbd80a66713dd13f6bfbb97e Mon Sep 17 00:00:00 2001
From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Date: Thu, 1 Oct 2026 18:00:13 -0400
Subject: [PATCH] feat(split-pane): resolve breakpoints from config
---
BREAKING.md | 5 +
core/api.txt | 2 +-
core/src/components.d.ts | 8 +-
core/src/components/split-pane/split-pane.tsx | 42 +++-
.../split-pane/test/when/index.html | 127 ++++++++++++
.../split-pane/test/when/split-pane.e2e.ts | 188 ++++++++++++++++++
6 files changed, 357 insertions(+), 15 deletions(-)
create mode 100644 core/src/components/split-pane/test/when/index.html
create mode 100644 core/src/components/split-pane/test/when/split-pane.e2e.ts
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/);
+ });
+ });
+ });
+});