From 7afba1af359fea4deb78c612320e1291610bb007 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 24 Apr 2026 16:03:45 -0700 Subject: [PATCH 1/8] feat(content): add recipe and tokens --- core/api.txt | 19 +++---- core/src/components.d.ts | 4 +- ...ent-interface.ts => content.interfaces.ts} | 22 +++++++++ core/src/components/content/content.scss | 49 +++++++++---------- core/src/components/content/content.tsx | 4 +- .../content/test/fullscreen/index.html | 4 +- .../content/test/standalone/index.html | 4 +- core/src/css/core.scss | 4 +- core/src/css/ionic/core.ionic.scss | 5 ++ core/src/global/config.ts | 4 +- core/src/themes/ionic/default.tokens.ts | 28 ++++++++++- core/src/themes/ios/default.tokens.ts | 26 ++++++++++ core/src/themes/md/default.tokens.ts | 26 ++++++++++ core/src/themes/themes.interfaces.ts | 3 ++ 14 files changed, 154 insertions(+), 48 deletions(-) rename core/src/components/content/{content-interface.ts => content.interfaces.ts} (58%) diff --git a/core/api.txt b/core/api.txt index a8217994eef..c8bad82bd8c 100644 --- a/core/api.txt +++ b/core/api.txt @@ -758,15 +758,16 @@ ion-content,method,scrollToTop,scrollToTop(duration?: number) => Promise ion-content,event,ionScroll,ScrollDetail,true ion-content,event,ionScrollEnd,ScrollBaseDetail,true ion-content,event,ionScrollStart,ScrollBaseDetail,true -ion-content,css-prop,--background -ion-content,css-prop,--color -ion-content,css-prop,--keyboard-offset -ion-content,css-prop,--offset-bottom -ion-content,css-prop,--offset-top -ion-content,css-prop,--padding-bottom -ion-content,css-prop,--padding-end -ion-content,css-prop,--padding-start -ion-content,css-prop,--padding-top +ion-content,css-prop,--ion-content-background +ion-content,css-prop,--ion-content-color +ion-content,css-prop,--ion-content-overflow +ion-content,css-prop,--ion-content-padding-bottom +ion-content,css-prop,--ion-content-padding-end +ion-content,css-prop,--ion-content-padding-start +ion-content,css-prop,--ion-content-padding-top +ion-content,css-prop,--ion-content-transition-cover-background +ion-content,css-prop,--ion-content-transition-cover-opacity +ion-content,css-prop,--ion-content-transition-shadow ion-content,part,background ion-content,part,scroll diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 5d1eff6638c..84043a0f2aa 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -15,7 +15,7 @@ import { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface"; import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface"; import { IonChipFill, IonChipHue, IonChipShape, IonChipSize } from "./components/chip/chip.interfaces"; -import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface"; +import { ScrollBaseDetail, ScrollDetail } from "./components/content/content.interfaces"; import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; import { SpinnerTypes } from "./components/spinner/spinner-configs"; import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; @@ -55,7 +55,7 @@ export { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface"; export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface"; export { IonChipFill, IonChipHue, IonChipShape, IonChipSize } from "./components/chip/chip.interfaces"; -export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface"; +export { ScrollBaseDetail, ScrollDetail } from "./components/content/content.interfaces"; export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; export { SpinnerTypes } from "./components/spinner/spinner-configs"; export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; diff --git a/core/src/components/content/content-interface.ts b/core/src/components/content/content.interfaces.ts similarity index 58% rename from core/src/components/content/content-interface.ts rename to core/src/components/content/content.interfaces.ts index c4e04431c2e..132387a2c30 100644 --- a/core/src/components/content/content-interface.ts +++ b/core/src/components/content/content.interfaces.ts @@ -1,4 +1,26 @@ import type { GestureDetail } from '../../interface'; +import type { IonPadding } from '../../themes/themes.interfaces'; + +export interface IonContentRecipe { + background?: string; + color?: string; + overflow?: string; + + padding?: IonPadding; + + transition?: { + cover?: { + background?: string; + opacity?: string; + }; + + shadow?: string; + }; +} + +export interface IonContentConfig { + transitionShadow?: boolean; +} export interface ScrollBaseDetail { isScrolling: boolean; diff --git a/core/src/components/content/content.scss b/core/src/components/content/content.scss index de81ebc89b4..31b7c679ded 100644 --- a/core/src/components/content/content.scss +++ b/core/src/components/content/content.scss @@ -5,30 +5,25 @@ :host { /** - * @prop --background: Background of the content + * @prop --ion-content-background: Background of the content * - * @prop --color: Color of the content + * @prop --ion-content-color: Color of the content * - * @prop --padding-top: Top padding of the content - * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the content - * @prop --padding-bottom: Bottom padding of the content - * @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the content + * @prop --ion-content-overflow: Overflow behavior of the scrollable area * - * @prop --keyboard-offset: Keyboard offset of the content + * @prop --ion-content-padding-top: Top padding of the content + * @prop --ion-content-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the content + * @prop --ion-content-padding-bottom: Bottom padding of the content + * @prop --ion-content-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the content * - * @prop --offset-top: Offset top of the content - * @prop --offset-bottom: Offset bottom of the content + * @prop --ion-content-transition-cover-background: Background color of the navigation transition cover overlay + * @prop --ion-content-transition-cover-opacity: Opacity of the navigation transition cover overlay + * + * @prop --ion-content-transition-shadow: Box shadow of the navigation transition shadow */ - --background: #{$background-color}; - --color: #{$text-color}; - --padding-top: 0px; - --padding-bottom: 0px; - --padding-start: 0px; - --padding-end: 0px; --keyboard-offset: 0px; --offset-top: 0px; --offset-bottom: 0px; - --overflow: auto; display: block; position: relative; @@ -59,21 +54,21 @@ position: absolute; - background: var(--background); + background: var(--ion-content-background); } .inner-scroll { @include position(calc(var(--offset-top) * -1), 0px, calc(var(--offset-bottom) * -1), 0px); @include padding( - calc(var(--padding-top) + var(--offset-top)), - var(--padding-end), - calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom)), - var(--padding-start) + calc(var(--ion-content-padding-top) + var(--offset-top)), + var(--ion-content-padding-end), + calc(var(--ion-content-padding-bottom) + var(--keyboard-offset) + var(--offset-bottom)), + var(--ion-content-padding-start) ); position: absolute; - color: var(--color); + color: var(--ion-content-color); box-sizing: border-box; @@ -115,12 +110,12 @@ } .scroll-y { - overflow-y: var(--overflow); + overflow-y: var(--ion-content-overflow); overscroll-behavior-y: contain; } .scroll-x { - overflow-x: var(--overflow); + overflow-x: var(--ion-content-overflow); overscroll-behavior-x: contain; } @@ -210,9 +205,9 @@ width: 100%; height: 100%; - background: black; + background: var(--ion-content-transition-cover-background); - opacity: 0.1; + opacity: var(--ion-content-transition-cover-opacity); } .transition-shadow { @@ -222,7 +217,7 @@ width: 100%; height: 100%; - box-shadow: inset -9px 0 9px 0 rgba(0, 0, 100, 0.03); + box-shadow: var(--ion-content-transition-shadow); } :host(.content-ltr) .transition-shadow { diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index 0c09660b9aa..f78b81916d2 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -10,7 +10,7 @@ import { config } from '../../global/config'; import { getIonMode, getIonTheme } from '../../global/ionic-global'; import type { Color, Mode } from '../../interface'; -import type { ScrollBaseDetail, ScrollDetail } from './content-interface'; +import type { ScrollBaseDetail, ScrollDetail } from './content.interfaces'; /** * @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component. @@ -457,7 +457,7 @@ export class Content implements ComponentInterface { const theme = getIonTheme(this); const mode = getIonMode(this); const forceOverscroll = this.shouldForceOverscroll(mode); - const transitionShadow = theme === 'ios'; + const transitionShadow = config.getObjectValue('IonContent.transitionShadow', false) as boolean; this.resize(); diff --git a/core/src/components/content/test/fullscreen/index.html b/core/src/components/content/test/fullscreen/index.html index bf0cebade91..8fa6eddfbe9 100644 --- a/core/src/components/content/test/fullscreen/index.html +++ b/core/src/components/content/test/fullscreen/index.html @@ -65,8 +65,8 @@ } ion-content { - --background: linear-gradient(90deg, blue, red); - --color: white; + --ion-content-background: linear-gradient(90deg, blue, red); + --ion-content-color: white; } p:first-child { diff --git a/core/src/components/content/test/standalone/index.html b/core/src/components/content/test/standalone/index.html index daed419034d..b492eb36ba7 100644 --- a/core/src/components/content/test/standalone/index.html +++ b/core/src/components/content/test/standalone/index.html @@ -59,8 +59,8 @@
Heading
} .custom-color { - --background: blue; - --color: white; + --ion-content-background: blue; + --ion-content-color: white; --hr-background: purple; } diff --git a/core/src/css/core.scss b/core/src/css/core.scss index b80d307d56e..ba8923075fb 100644 --- a/core/src/css/core.scss +++ b/core/src/css/core.scss @@ -247,8 +247,8 @@ ion-card-header.ion-color .ion-inherit-color { * The code below accounts for both ion-content and then custom * scroll containers within ion-content (such as virtual scroll) */ -.menu-content-open ion-content { - --overflow: hidden; +.menu-content-open ion-content::part(scroll) { + overflow: hidden; } .menu-content-open .ion-content-scroll-host { diff --git a/core/src/css/ionic/core.ionic.scss b/core/src/css/ionic/core.ionic.scss index f42a9b0ff1e..19de110b49e 100644 --- a/core/src/css/ionic/core.ionic.scss +++ b/core/src/css/ionic/core.ionic.scss @@ -254,6 +254,11 @@ ion-card-header.ion-color .ion-inherit-color { * The code below accounts for both ion-content and then custom * scroll containers within ion-content (such as virtual scroll) */ +/** + * NOTE: This rule will not be updated as part of individual component migrations. + * core.ionic.scss is slated for deletion and will be fully replaced by core.scss. + * All remaining styles here will be consolidated into core.scss at that time. + */ .menu-content-open ion-content { --overflow: hidden; } diff --git a/core/src/global/config.ts b/core/src/global/config.ts index d9795de6495..9cb262a4bea 100644 --- a/core/src/global/config.ts +++ b/core/src/global/config.ts @@ -2,6 +2,8 @@ import type { IonicConfig } from '../themes/themes.interfaces'; // TODO(FW-2832): types +type ObjectConfigValue = string | boolean; + export class Config { private m = new Map(); @@ -21,7 +23,7 @@ export class Config { * @param fallback Default value if the key is not found * @returns The value found at the nested key or the fallback */ - getObjectValue(key: string, fallback?: string): string | undefined { + getObjectValue(key: string, fallback?: ObjectConfigValue): ObjectConfigValue | undefined { const [firstKey, ...remainingKeys] = key.split('.'); let root: any; diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index e4d4407e2b6..469277a761c 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -1,4 +1,4 @@ -import { currentColor, mix, dynamicFont } from '../../utils/theme'; +import { rgba, currentColor, mix, dynamicFont } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import { colors as baseColors } from '../base/shared.tokens'; import type { DefaultTheme } from '../themes.interfaces'; @@ -28,6 +28,10 @@ export const defaultTheme: DefaultTheme = { size: 'large', }, + IonContent: { + transitionShadow: false, + }, + IonSpinner: { size: 'xsmall', }, @@ -268,6 +272,28 @@ export const defaultTheme: DefaultTheme = { }, }, + IonContent: { + background: baseColors.backgroundColor, + color: baseColors.textColor, + overflow: 'auto', + + padding: { + bottom: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + top: 'var(--ion-spacing-0)', + }, + + transition: { + cover: { + background: baseColors.black, + opacity: '0.1', + }, + + shadow: `inset -9px 0 9px 0 ${rgba('0, 0, 100', 0.03)}`, + }, + }, + IonItemDivider: { background: baseColors.backgroundColor, color: `var(--ion-text-color-step-600, ${mix(baseColors.white, baseColors.black, '40%')})`, diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index f79dd62558b..fac62335ec9 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -30,6 +30,10 @@ export const defaultTheme: DefaultTheme = { size: 'large', }, + IonContent: { + transitionShadow: true, + }, + IonSpinner: { size: 'medium', }, @@ -402,6 +406,28 @@ export const defaultTheme: DefaultTheme = { }, }, + IonContent: { + background: baseColors.backgroundColor, + color: baseColors.textColor, + overflow: 'auto', + + padding: { + bottom: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + top: 'var(--ion-spacing-0)', + }, + + transition: { + cover: { + background: baseColors.black, + opacity: '0.1', + }, + + shadow: `inset -9px 0 9px 0 ${rgba('0, 0, 100', 0.03)}`, + }, + }, + IonItemDivider: { background: `var(--ion-background-color-step-100, ${mix(baseColors.black, baseColors.white, '90%')})`, color: `var(--ion-text-color-step-150, ${mix(baseColors.white, baseColors.black, '85%')})`, diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index b978c59ef9b..daca05b48c4 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -33,6 +33,10 @@ export const defaultTheme: DefaultTheme = { size: 'large', }, + IonContent: { + transitionShadow: false, + }, + IonSpinner: { size: 'medium', }, @@ -399,6 +403,28 @@ export const defaultTheme: DefaultTheme = { }, }, + IonContent: { + background: baseColors.backgroundColor, + color: baseColors.textColor, + overflow: 'auto', + + padding: { + bottom: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + top: 'var(--ion-spacing-0)', + }, + + transition: { + cover: { + background: baseColors.black, + opacity: '0.1', + }, + + shadow: `inset -9px 0 9px 0 ${rgba('0, 0, 100', 0.03)}`, + }, + }, + IonItemDivider: { background: baseColors.backgroundColor, color: `var(--ion-text-color-step-600, ${mix(baseColors.white, baseColors.black, '40%')})`, diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index e7e8685cff9..051dc030ecc 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -1,4 +1,5 @@ import type { IonChipConfig, IonChipRecipe } from '../components/chip/chip.interfaces'; +import type { IonContentConfig, IonContentRecipe } from '../components/content/content.interfaces'; import type { IonItemDividerRecipe } from '../components/item-divider/item-divider.interfaces'; import type { IonSpinnerConfig, IonSpinnerRecipe } from '../components/spinner/spinner.interfaces'; import type { IonicConfig as IonicGlobalConfig } from '../utils/config'; @@ -243,6 +244,7 @@ export type BaseTheme = { export type IonicConfig = IonicGlobalConfig & { components?: { IonChip?: IonChipConfig; + IonContent?: IonContentConfig; IonSpinner?: IonSpinnerConfig; }; }; @@ -281,6 +283,7 @@ export type DefaultTheme = BaseTheme & { type Components = { IonChip?: IonChipRecipe; + IonContent?: IonContentRecipe; IonItemDivider?: IonItemDividerRecipe; IonSpinner?: IonSpinnerRecipe; From 32d2fdb70484244d876eb951c5dc67dcbd7e6bfc Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Apr 2026 10:00:14 -0700 Subject: [PATCH 2/8] feat(content): cleanup --- core/src/components.d.ts | 8 -- .../components/content/content.interfaces.ts | 9 +- core/src/components/content/content.scss | 118 ++++++++++-------- core/src/components/content/content.tsx | 21 ++-- .../components/content/test/basic/index.html | 4 - core/src/components/refresher/refresher.tsx | 4 +- core/src/interface.d.ts | 2 +- core/src/themes/ionic/default.tokens.ts | 9 +- core/src/themes/ios/default.tokens.ts | 9 +- core/src/themes/md/default.tokens.ts | 9 +- .../utils/input-shims/hacks/scroll-padding.ts | 4 +- .../hacks/test/scroll-assist.e2e.ts | 4 +- 12 files changed, 108 insertions(+), 93 deletions(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 84043a0f2aa..3b7f07e4dde 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1118,10 +1118,6 @@ export namespace Components { * @default true */ "scrollY": boolean; - /** - * The theme determines the visual appearance of the component. - */ - "theme"?: "ios" | "md" | "ionic"; } interface IonDatetime { /** @@ -7060,10 +7056,6 @@ declare namespace LocalJSX { * @default true */ "scrollY"?: boolean; - /** - * The theme determines the visual appearance of the component. - */ - "theme"?: "ios" | "md" | "ionic"; } interface IonDatetime { /** diff --git a/core/src/components/content/content.interfaces.ts b/core/src/components/content/content.interfaces.ts index 132387a2c30..2a14533cc16 100644 --- a/core/src/components/content/content.interfaces.ts +++ b/core/src/components/content/content.interfaces.ts @@ -4,6 +4,11 @@ import type { IonPadding } from '../../themes/themes.interfaces'; export interface IonContentRecipe { background?: string; color?: string; + + font?: { + family?: string; + }; + overflow?: string; padding?: IonPadding; @@ -18,9 +23,7 @@ export interface IonContentRecipe { }; } -export interface IonContentConfig { - transitionShadow?: boolean; -} +export interface IonContentConfig {} export interface ScrollBaseDetail { isScrolling: boolean; diff --git a/core/src/components/content/content.scss b/core/src/components/content/content.scss index 31b7c679ded..070b03d0ce5 100644 --- a/core/src/components/content/content.scss +++ b/core/src/components/content/content.scss @@ -1,4 +1,5 @@ -@import "../../themes/native/native.globals"; +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.color" as color; // Content // -------------------------------------------------- @@ -9,21 +10,18 @@ * * @prop --ion-content-color: Color of the content * + * @prop --ion-content-font-family: Font family of the content + * * @prop --ion-content-overflow: Overflow behavior of the scrollable area * * @prop --ion-content-padding-top: Top padding of the content * @prop --ion-content-padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the content * @prop --ion-content-padding-bottom: Bottom padding of the content * @prop --ion-content-padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the content - * - * @prop --ion-content-transition-cover-background: Background color of the navigation transition cover overlay - * @prop --ion-content-transition-cover-opacity: Opacity of the navigation transition cover overlay - * - * @prop --ion-content-transition-shadow: Box shadow of the navigation transition shadow */ - --keyboard-offset: 0px; - --offset-top: 0px; - --offset-bottom: 0px; + --internal-keyboard-offset: 0px; + --internal-offset-top: 0px; + --internal-offset-bottom: 0px; display: block; position: relative; @@ -39,30 +37,20 @@ padding: 0 !important; /* stylelint-enable */ - font-family: $font-family-base; + font-family: var(--ion-content-font-family); contain: size style; } -:host(.ion-color) .inner-scroll { - background: current-color(base); - color: current-color(contrast); -} - -#background-content { - @include position(calc(var(--offset-top) * -1), 0px, calc(var(--offset-bottom) * -1), 0px); - - position: absolute; - - background: var(--ion-content-background); -} +// Content Inner Scroll +// --------------------------------------------- .inner-scroll { - @include position(calc(var(--offset-top) * -1), 0px, calc(var(--offset-bottom) * -1), 0px); - @include padding( - calc(var(--ion-content-padding-top) + var(--offset-top)), + @include mixins.position(calc(var(--internal-offset-top) * -1), 0px, calc(var(--internal-offset-bottom) * -1), 0px); + @include mixins.padding( + calc(var(--ion-content-padding-top) + var(--internal-offset-top)), var(--ion-content-padding-end), - calc(var(--ion-content-padding-bottom) + var(--keyboard-offset) + var(--offset-bottom)), + calc(var(--ion-content-padding-bottom) + var(--internal-keyboard-offset) + var(--internal-offset-bottom)), var(--ion-content-padding-start) ); @@ -84,6 +72,14 @@ touch-action: pan-x pan-y pinch-zoom; } +:host(.ion-color) .inner-scroll { + background: color.current-color(base); + color: color.current-color(contrast); +} + +// Content Scroll +// --------------------------------------------- + .scroll-y, .scroll-x { -webkit-overflow-scrolling: touch; @@ -119,6 +115,20 @@ overscroll-behavior-x: contain; } +// Content Background +// --------------------------------------------- + +#background-content { + @include mixins.position(calc(var(--internal-offset-top) * -1), 0px, calc(var(--internal-offset-bottom) * -1), 0px); + + position: absolute; + + background: var(--ion-content-background); +} + +// Content Overscroll +// --------------------------------------------- + .overscroll::before, .overscroll::after { position: absolute; @@ -137,6 +147,9 @@ top: -1px; } +// Content Sizing +// --------------------------------------------- + :host(.content-sizing) { display: flex; @@ -154,6 +167,7 @@ contain: none; } + :host(.content-sizing) .inner-scroll { position: relative; @@ -167,10 +181,34 @@ top: 0; bottom: 0; - margin-top: calc(var(--offset-top) * -1); - margin-bottom: calc(var(--offset-bottom) * -1); + margin-top: calc(var(--internal-offset-top) * -1); + margin-bottom: calc(var(--internal-offset-bottom) * -1); } +// Content Slotted Elements +// --------------------------------------------- + +// Elements with the "fixed" slot +::slotted([slot="fixed"]) { + position: absolute; + + /** + * When presenting ion-content inside of an ion-modal, the .inner-scroll + * element is composited. In WebKit, the fixed content is not composited + * causing it to appear under the main scrollable content as a result. + * The fixed content is correctly composited in other browsers. Adding + * the translateZ forces the fixed content to be composited so it correctly + * shows on top of the scrollable content. Setting a negative z-index will + * still allow the fixed content to appear under the scroll content if specified. + */ + transform: translateZ(0); +} + +// Content: iOS Mode Transition +// The transition shadow effect is only animated by the iOS transition +// builder, so these styles are only rendered in iOS mode. +// --------------------------------------------- + .transition-effect { display: none; position: absolute; @@ -205,9 +243,9 @@ width: 100%; height: 100%; - background: var(--ion-content-transition-cover-background); + background: black; - opacity: var(--ion-content-transition-cover-opacity); + opacity: 0.1; } .transition-shadow { @@ -217,7 +255,7 @@ width: 100%; height: 100%; - box-shadow: var(--ion-content-transition-shadow); + box-shadow: inset -9px 0 9px 0 rgba(0, 0, 100, 0.03); } :host(.content-ltr) .transition-shadow { @@ -233,21 +271,3 @@ transform: scaleX(-1); } - -// Content: Fixed -// -------------------------------------------------- - -::slotted([slot="fixed"]) { - position: absolute; - - /** - * When presenting ion-content inside of an ion-modal, the .inner-scroll - * element is composited. In WebKit, the fixed content is not composited - * causing it to appear under the main scrollable content as a result. - * The fixed content is correctly composited in other browsers. Adding - * the translateZ forces the fixed content to be composited so it correctly - * shows on top of the scrollable content. Setting a negative z-index will - * still allow the fixed content to appear under the scroll content if specified. - */ - transform: translateZ(0); -} diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index f78b81916d2..818da07c768 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -7,14 +7,13 @@ import { isRTL } from '@utils/rtl'; import { createColorClasses, hostContext } from '@utils/theme'; import { config } from '../../global/config'; -import { getIonMode, getIonTheme } from '../../global/ionic-global'; +import { getIonMode } from '../../global/ionic-global'; import type { Color, Mode } from '../../interface'; import type { ScrollBaseDetail, ScrollDetail } from './content.interfaces'; /** * @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component. - * @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component. * * @slot - Content is placed in the scrollable area if provided without a slot. * @slot fixed - Should be used for fixed content that should not scroll. @@ -245,7 +244,7 @@ export class Content implements ComponentInterface { * The `hydrateDocument` function in `@stencil/core` will render the `ion-content`, but * `forceUpdate` will trigger another render, locking up the server. * - * TODO: Remove if STENCIL-834 determines Stencil will account for this. + * TODO(STENCIL-834): Remove if Stencil will account for this. */ if (Build.isBrowser) { if (this.fullscreen) { @@ -349,7 +348,7 @@ export class Content implements ComponentInterface { @Method() async scrollToBottom(duration = 0): Promise { const scrollEl = await this.getScrollElement(); - const y = scrollEl!.scrollHeight - scrollEl!.clientHeight; + const y = scrollEl.scrollHeight - scrollEl.clientHeight; return this.scrollToPoint(undefined, y, duration); } @@ -454,10 +453,13 @@ export class Content implements ComponentInterface { render() { const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this; const rtl = isRTL(el) ? 'rtl' : 'ltr'; - const theme = getIonTheme(this); const mode = getIonMode(this); const forceOverscroll = this.shouldForceOverscroll(mode); - const transitionShadow = config.getObjectValue('IonContent.transitionShadow', false) as boolean; + /** + * The transition shadow effect is only animated by the iOS transition + * builder, so these elements are only rendered in iOS mode. + */ + const transitionShadow = mode === 'ios'; this.resize(); @@ -465,14 +467,13 @@ export class Content implements ComponentInterface { diff --git a/core/src/components/content/test/basic/index.html b/core/src/components/content/test/basic/index.html index 8c65e3b82e7..663dd11ca4b 100644 --- a/core/src/components/content/test/basic/index.html +++ b/core/src/components/content/test/basic/index.html @@ -135,10 +135,6 @@ f:last-of-type { background: yellow; } - - ion-content#content-part::part(background) { - background: #eee; - } diff --git a/core/src/components/refresher/refresher.tsx b/core/src/components/refresher/refresher.tsx index 7a6e66b232e..dcf1e565b2b 100644 --- a/core/src/components/refresher/refresher.tsx +++ b/core/src/components/refresher/refresher.tsx @@ -604,7 +604,7 @@ export class Refresher implements ComponentInterface { * element to ensure that the refresher is shown. */ if (this.contentFullscreen && this.backgroundContentEl) { - this.backgroundContentEl.style.setProperty('--offset-top', '0px'); + this.backgroundContentEl.style.setProperty('--internal-offset-top', '0px'); } } @@ -783,7 +783,7 @@ export class Refresher implements ComponentInterface { * does not change when refreshing is complete. */ if (this.contentFullscreen && this.backgroundContentEl) { - this.backgroundContentEl?.style.removeProperty('--offset-top'); + this.backgroundContentEl?.style.removeProperty('--internal-offset-top'); } }, 600); diff --git a/core/src/interface.d.ts b/core/src/interface.d.ts index 158b8ad1c19..8e303941aee 100644 --- a/core/src/interface.d.ts +++ b/core/src/interface.d.ts @@ -7,7 +7,7 @@ export { AccordionGroupCustomEvent } from './components/accordion-group/accordio export { AlertOptions } from './components/alert/alert-interface'; export { ActionSheetOptions } from './components/action-sheet/action-sheet-interface'; export { BreadcrumbCustomEvent } from './components/breadcrumb/breadcrumb-interface'; -export { ScrollBaseCustomEvent, ScrollCallback, ScrollCustomEvent } from './components/content/content-interface'; +export { ScrollBaseCustomEvent, ScrollCallback, ScrollCustomEvent } from './components/content/content.interfaces'; export { CheckboxCustomEvent } from './components/checkbox/checkbox-interface'; export { DatetimeCustomEvent, DatetimeHighlightStyle } from './components/datetime/datetime-interface'; export { InfiniteScrollCustomEvent } from './components/infinite-scroll/infinite-scroll-interface'; diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index 469277a761c..b068167673b 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -28,10 +28,6 @@ export const defaultTheme: DefaultTheme = { size: 'large', }, - IonContent: { - transitionShadow: false, - }, - IonSpinner: { size: 'xsmall', }, @@ -275,6 +271,11 @@ export const defaultTheme: DefaultTheme = { IonContent: { background: baseColors.backgroundColor, color: baseColors.textColor, + + font: { + family: 'var(--ion-font-family, inherit)', + }, + overflow: 'auto', padding: { diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index fac62335ec9..72eaff3307d 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -30,10 +30,6 @@ export const defaultTheme: DefaultTheme = { size: 'large', }, - IonContent: { - transitionShadow: true, - }, - IonSpinner: { size: 'medium', }, @@ -409,6 +405,11 @@ export const defaultTheme: DefaultTheme = { IonContent: { background: baseColors.backgroundColor, color: baseColors.textColor, + + font: { + family: 'var(--ion-font-family, inherit)', + }, + overflow: 'auto', padding: { diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index daca05b48c4..96d0c79d513 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -33,10 +33,6 @@ export const defaultTheme: DefaultTheme = { size: 'large', }, - IonContent: { - transitionShadow: false, - }, - IonSpinner: { size: 'medium', }, @@ -406,6 +402,11 @@ export const defaultTheme: DefaultTheme = { IonContent: { background: baseColors.backgroundColor, color: baseColors.textColor, + + font: { + family: 'var(--ion-font-family, inherit)', + }, + overflow: 'auto', padding: { diff --git a/core/src/utils/input-shims/hacks/scroll-padding.ts b/core/src/utils/input-shims/hacks/scroll-padding.ts index 8dc266972f2..8216d0f6cbe 100644 --- a/core/src/utils/input-shims/hacks/scroll-padding.ts +++ b/core/src/utils/input-shims/hacks/scroll-padding.ts @@ -26,10 +26,10 @@ export const setScrollPadding = (contentEl: HTMLElement, paddingAmount: number, } if (paddingAmount > 0) { - contentEl.style.setProperty('--keyboard-offset', `${paddingAmount}px`); + contentEl.style.setProperty('--internal-keyboard-offset', `${paddingAmount}px`); } else { (contentEl as any)[PADDING_TIMER_KEY] = setTimeout(() => { - contentEl.style.setProperty('--keyboard-offset', '0px'); + contentEl.style.setProperty('--internal-keyboard-offset', '0px'); if (clearCallback) { clearCallback(); } diff --git a/core/src/utils/input-shims/hacks/test/scroll-assist.e2e.ts b/core/src/utils/input-shims/hacks/test/scroll-assist.e2e.ts index 9edfc45e7d2..2a45d285c92 100644 --- a/core/src/utils/input-shims/hacks/test/scroll-assist.e2e.ts +++ b/core/src/utils/input-shims/hacks/test/scroll-assist.e2e.ts @@ -167,7 +167,7 @@ class ScrollAssistFixture { await this.focusInput(interactiveSelector, inputSelector); - await expect(content).not.toHaveCSS('--keyboard-offset', '0px'); + await expect(content).not.toHaveCSS('--internal-keyboard-offset', '0px'); } async expectNotToHaveScrollPadding(interactiveSelector: string, inputSelector: string) { @@ -175,6 +175,6 @@ class ScrollAssistFixture { await this.focusInput(interactiveSelector, inputSelector); - await expect(content).toHaveCSS('--keyboard-offset', '0px'); + await expect(content).toHaveCSS('--internal-keyboard-offset', '0px'); } } From 57c31404681e88e83ec70d2bdec7c8d8e5fe0e7b Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Apr 2026 10:01:30 -0700 Subject: [PATCH 3/8] feat(content): more cleanup --- core/api.txt | 5 +---- packages/angular/src/directives/proxies.ts | 4 ++-- packages/angular/standalone/src/directives/proxies.ts | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/core/api.txt b/core/api.txt index c8bad82bd8c..640b16cfa69 100644 --- a/core/api.txt +++ b/core/api.txt @@ -749,7 +749,6 @@ ion-content,prop,mode,"ios" | "md",undefined,false,false ion-content,prop,scrollEvents,boolean,false,false,false ion-content,prop,scrollX,boolean,false,false,false ion-content,prop,scrollY,boolean,true,false,false -ion-content,prop,theme,"ios" | "md" | "ionic",undefined,false,false ion-content,method,getScrollElement,getScrollElement() => Promise ion-content,method,scrollByPoint,scrollByPoint(x: number, y: number, duration: number) => Promise ion-content,method,scrollToBottom,scrollToBottom(duration?: number) => Promise @@ -760,14 +759,12 @@ ion-content,event,ionScrollEnd,ScrollBaseDetail,true ion-content,event,ionScrollStart,ScrollBaseDetail,true ion-content,css-prop,--ion-content-background ion-content,css-prop,--ion-content-color +ion-content,css-prop,--ion-content-font-family ion-content,css-prop,--ion-content-overflow ion-content,css-prop,--ion-content-padding-bottom ion-content,css-prop,--ion-content-padding-end ion-content,css-prop,--ion-content-padding-start ion-content,css-prop,--ion-content-padding-top -ion-content,css-prop,--ion-content-transition-cover-background -ion-content,css-prop,--ion-content-transition-cover-opacity -ion-content,css-prop,--ion-content-transition-shadow ion-content,part,background ion-content,part,scroll diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index b1073a2cfbf..49e169a5ab7 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -594,7 +594,7 @@ export declare interface IonCol extends Components.IonCol {} @ProxyCmp({ - inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY', 'theme'], + inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY'], methods: ['getScrollElement', 'scrollToTop', 'scrollToBottom', 'scrollByPoint', 'scrollToPoint'] }) @Component({ @@ -602,7 +602,7 @@ export declare interface IonCol extends Components.IonCol {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY', 'theme'], + inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY'], }) export class IonContent { protected el: HTMLIonContentElement; diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index 62d708102b3..ee9139155c5 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -666,7 +666,7 @@ export declare interface IonCol extends Components.IonCol {} @ProxyCmp({ defineCustomElementFn: defineIonContent, - inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY', 'theme'], + inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY'], methods: ['getScrollElement', 'scrollToTop', 'scrollToBottom', 'scrollByPoint', 'scrollToPoint'] }) @Component({ @@ -674,7 +674,7 @@ export declare interface IonCol extends Components.IonCol {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY', 'theme'], + inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY'], standalone: true }) export class IonContent { From 5983c3b6ac8feeccf10b7f374c0a071436d73753 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Apr 2026 10:34:56 -0700 Subject: [PATCH 4/8] refactor(utils): created waitForComponent --- core/src/components/content/content.tsx | 11 +++++------ core/src/components/input/input.tsx | 4 ++-- core/src/components/refresher/refresher.utils.ts | 4 ++-- core/src/components/router/utils/dom.ts | 4 ++-- core/src/components/searchbar/searchbar.tsx | 4 ++-- core/src/components/textarea/textarea.tsx | 4 ++-- core/src/utils/content/index.ts | 4 ++-- core/src/utils/framework-delegate.ts | 6 +++--- core/src/utils/helpers.ts | 9 +++++++++ core/src/utils/input-shims/input-shims.ts | 4 ++-- core/src/utils/menu-controller/index.ts | 8 ++------ core/src/utils/overlays.ts | 6 +++--- core/src/utils/status-tap.ts | 4 ++-- 13 files changed, 38 insertions(+), 34 deletions(-) diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index 818da07c768..fc84f4432e7 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -1,6 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Build, Component, Element, Event, Host, Listen, Method, Prop, forceUpdate, h, readTask } from '@stencil/core'; -import { componentOnReady, hasLazyBuild, inheritAriaAttributes } from '@utils/helpers'; +import { hasLazyBuild, inheritAriaAttributes, waitForComponent } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; import { isPlatform } from '@utils/platform'; import { isRTL } from '@utils/rtl'; @@ -312,10 +312,9 @@ export class Content implements ComponentInterface { * scrollEl won't be defined yet with the custom elements build, so wait for it to load in. */ if (!this.scrollEl) { - await new Promise((resolve) => componentOnReady(this.el, resolve)); + await waitForComponent(this.el); } - - return Promise.resolve(this.scrollEl!); + return this.scrollEl!; } /** @@ -325,9 +324,9 @@ export class Content implements ComponentInterface { @Method() async getBackgroundElement(): Promise { if (!this.backgroundContentEl) { - await new Promise((resolve) => componentOnReady(this.el, resolve)); + await waitForComponent(this.el); } - return Promise.resolve(this.backgroundContentEl!); + return this.backgroundContentEl!; } /** diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 94829b29bdd..d2b2cbbd299 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -17,7 +17,7 @@ import { import type { NotchController } from '@utils/forms'; import { createNotchController, checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; -import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers'; +import { inheritAriaAttributes, debounceEvent, inheritAttributes, waitForComponent } from '@utils/helpers'; import { printIonWarning } from '@utils/logging'; import { createSlotMutationController } from '@utils/slot-mutation-controller'; import type { SlotMutationController } from '@utils/slot-mutation-controller'; @@ -537,7 +537,7 @@ export class Input implements ComponentInterface { * nativeInput won't be defined yet with the custom elements build, so wait for it to load in. */ if (!this.nativeInput) { - await new Promise((resolve) => componentOnReady(this.el, resolve)); + await waitForComponent(this.el); } return Promise.resolve(this.nativeInput!); } diff --git a/core/src/components/refresher/refresher.utils.ts b/core/src/components/refresher/refresher.utils.ts index 4becab356d0..e2f178e6064 100644 --- a/core/src/components/refresher/refresher.utils.ts +++ b/core/src/components/refresher/refresher.utils.ts @@ -1,6 +1,6 @@ import { writeTask } from '@stencil/core'; import { createAnimation } from '@utils/animation/animation'; -import { clamp, componentOnReady, transitionEndAsync } from '@utils/helpers'; +import { clamp, waitForComponent, transitionEndAsync } from '@utils/helpers'; // MD Native Refresher // ----------------------------- @@ -219,7 +219,7 @@ export const shouldUseNativeRefresher = async (referenceEl: HTMLIonRefresherElem return Promise.resolve(false); } - await new Promise((resolve) => componentOnReady(refresherContent, resolve)); + await waitForComponent(refresherContent); const pullingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-pulling ion-spinner'); const refreshingSpinner = referenceEl.querySelector('ion-refresher-content .refresher-refreshing ion-spinner'); diff --git a/core/src/components/router/utils/dom.ts b/core/src/components/router/utils/dom.ts index c10b34d0840..7963e43b3d1 100644 --- a/core/src/components/router/utils/dom.ts +++ b/core/src/components/router/utils/dom.ts @@ -1,4 +1,4 @@ -import { componentOnReady } from '@utils/helpers'; +import { waitForComponent } from '@utils/helpers'; import { printIonError } from '@utils/logging'; import type { AnimationBuilder } from '../../../interface'; @@ -30,7 +30,7 @@ export const writeNavState = async ( if (index >= chain.length || !outlet) { return changed; } - await new Promise((resolve) => componentOnReady(outlet, resolve)); + await waitForComponent(outlet); const route = chain[index]; const result = await outlet.setRouteId(route.id, route.params, direction, animation); diff --git a/core/src/components/searchbar/searchbar.tsx b/core/src/components/searchbar/searchbar.tsx index 4d1047f914f..cbe2c754bee 100644 --- a/core/src/components/searchbar/searchbar.tsx +++ b/core/src/components/searchbar/searchbar.tsx @@ -3,7 +3,7 @@ import magnifyingGlassRegular from '@phosphor-icons/core/assets/regular/magnifyi import xRegular from '@phosphor-icons/core/assets/regular/x.svg'; import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Method, Prop, State, Watch, forceUpdate, h } from '@stencil/core'; -import { debounceEvent, raf, componentOnReady, inheritAttributes } from '@utils/helpers'; +import { debounceEvent, raf, waitForComponent, inheritAttributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; import { isRTL } from '@utils/rtl'; import { createColorClasses, hostContext } from '@utils/theme'; @@ -349,7 +349,7 @@ export class Searchbar implements ComponentInterface { * nativeInput won't be defined yet with the custom elements build, so wait for it to load in. */ if (!this.nativeInput) { - await new Promise((resolve) => componentOnReady(this.el, resolve)); + await waitForComponent(this.el); } return Promise.resolve(this.nativeInput!); } diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx index 272b21f3077..b119c94f1a7 100644 --- a/core/src/components/textarea/textarea.tsx +++ b/core/src/components/textarea/textarea.tsx @@ -17,7 +17,7 @@ import { import type { NotchController } from '@utils/forms'; import { createNotchController, checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; -import { inheritAriaAttributes, debounceEvent, inheritAttributes, componentOnReady } from '@utils/helpers'; +import { inheritAriaAttributes, debounceEvent, inheritAttributes, waitForComponent } from '@utils/helpers'; import { createSlotMutationController } from '@utils/slot-mutation-controller'; import type { SlotMutationController } from '@utils/slot-mutation-controller'; import { createColorClasses, hostContext } from '@utils/theme'; @@ -452,7 +452,7 @@ export class Textarea implements ComponentInterface { * nativeInput won't be defined yet with the custom elements build, so wait for it to load in. */ if (!this.nativeInput) { - await new Promise((resolve) => componentOnReady(this.el, resolve)); + await waitForComponent(this.el); } return Promise.resolve(this.nativeInput!); } diff --git a/core/src/utils/content/index.ts b/core/src/utils/content/index.ts index 44a6f7bff13..0a73a672b5a 100644 --- a/core/src/utils/content/index.ts +++ b/core/src/utils/content/index.ts @@ -1,4 +1,4 @@ -import { componentOnReady } from '../helpers'; +import { waitForComponent } from '../helpers'; import { printRequiredElementError } from '../logging'; const ION_CONTENT_TAG_NAME = 'ION-CONTENT'; @@ -27,7 +27,7 @@ export const isIonContent = (el: Element) => el.tagName === ION_CONTENT_TAG_NAME */ export const getScrollElement = async (el: Element) => { if (isIonContent(el)) { - await new Promise((resolve) => componentOnReady(el, resolve)); + await waitForComponent(el); return (el as HTMLIonContentElement).getScrollElement(); } diff --git a/core/src/utils/framework-delegate.ts b/core/src/utils/framework-delegate.ts index 37616c037ac..ad858d2e636 100644 --- a/core/src/utils/framework-delegate.ts +++ b/core/src/utils/framework-delegate.ts @@ -1,7 +1,7 @@ import { config } from '../global/config'; import type { ComponentRef, FrameworkDelegate } from '../interface'; -import { componentOnReady } from './helpers'; +import { waitForComponent } from './helpers'; // TODO(FW-2832): types @@ -31,7 +31,7 @@ export const attachComponent = async ( container.appendChild(el); - await new Promise((resolve) => componentOnReady(el, resolve)); + await waitForComponent(el); return el; }; @@ -91,7 +91,7 @@ export const CoreDelegate = () => { ChildComponent = el; - await new Promise((resolve) => componentOnReady(el, resolve)); + await waitForComponent(el); } else if ( BaseComponent.children.length > 0 && (BaseComponent.tagName === 'ION-MODAL' || BaseComponent.tagName === 'ION-POPOVER') diff --git a/core/src/utils/helpers.ts b/core/src/utils/helpers.ts index 17a563b601e..d3a3d327d05 100644 --- a/core/src/utils/helpers.ts +++ b/core/src/utils/helpers.ts @@ -83,6 +83,15 @@ export const componentOnReady = (el: any, callback: any) => { } }; +/** + * Promise-based wrapper around componentOnReady. Use when you need to await + * component readiness before accessing internal refs (e.g. in early lifecycle + * hooks like Vue onMounted with the custom elements build). + */ +export const waitForComponent = (el: T): Promise => { + return new Promise((resolve) => componentOnReady(el, () => resolve(el))); +}; + /** * This functions checks if a Stencil component is using * the lazy loaded build of Stencil. Returns `true` if diff --git a/core/src/utils/input-shims/input-shims.ts b/core/src/utils/input-shims/input-shims.ts index 879fbfff870..9f7879790af 100644 --- a/core/src/utils/input-shims/input-shims.ts +++ b/core/src/utils/input-shims/input-shims.ts @@ -2,7 +2,7 @@ import { doc } from '@utils/browser'; import type { Config } from '../../interface'; import { findClosestIonContent } from '../content'; -import { componentOnReady } from '../helpers'; +import { waitForComponent } from '../helpers'; import { Keyboard } from '../native/keyboard'; import { enableHideCaretOnScroll } from './hacks/hide-caret'; @@ -59,7 +59,7 @@ export const startInputShims = async (config: Config, platform: 'ios' | 'android const keyboardResizeMode = await Keyboard.getResizeMode(); const registerInput = async (componentEl: HTMLElement) => { - await new Promise((resolve) => componentOnReady(componentEl, resolve)); + await waitForComponent(componentEl); const inputRoot = componentEl.shadowRoot || componentEl; const inputEl = inputRoot.querySelector('input') || inputRoot.querySelector('textarea'); diff --git a/core/src/utils/menu-controller/index.ts b/core/src/utils/menu-controller/index.ts index dca6c5bc8ce..bdb72963f41 100644 --- a/core/src/utils/menu-controller/index.ts +++ b/core/src/utils/menu-controller/index.ts @@ -5,7 +5,7 @@ import { printIonWarning } from '@utils/logging'; import type { MenuI, MenuControllerI } from '../../components/menu/menu-interface'; import type { AnimationBuilder } from '../../interface'; -import { componentOnReady } from '../helpers'; +import { waitForComponent } from '../helpers'; import { menuOverlayAnimation } from './animations/overlay'; import { menuPushAnimation } from './animations/push'; @@ -218,11 +218,7 @@ const createMenuController = (): MenuControllerI => { }; const waitUntilReady = () => { - return Promise.all( - Array.from(document.querySelectorAll('ion-menu')).map( - (menu) => new Promise((resolve) => componentOnReady(menu, resolve)) - ) - ); + return Promise.all(Array.from(document.querySelectorAll('ion-menu')).map((menu) => waitForComponent(menu))); }; registerAnimation('reveal', menuRevealAnimation); diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 1271a999285..25485bf0ea7 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -27,10 +27,10 @@ import { BACKDROP_NO_SCROLL } from './gesture/gesture-controller'; import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button'; import { addEventListener, - componentOnReady, focusVisibleElement, getElementRoot, removeEventListener, + waitForComponent, } from './helpers'; let lastOverlayIndex = 0; @@ -126,7 +126,7 @@ export const createOverlay = ( // eslint-disable-next-line @typescript-eslint/prefer-optional-chain if (typeof window !== 'undefined' && typeof window.customElements !== 'undefined') { return window.customElements.whenDefined(tagName).then(() => { - const element = document.createElement(tagName) as HTMLIonOverlayElement; + const element = document.createElement(tagName) as T; element.classList.add('overlay-hidden'); /** @@ -138,7 +138,7 @@ export const createOverlay = ( // append the overlay element to the document body getAppRoot(document).appendChild(element); - return new Promise((resolve) => componentOnReady(element, resolve)); + return waitForComponent(element); }); } return Promise.resolve() as any; diff --git a/core/src/utils/status-tap.ts b/core/src/utils/status-tap.ts index 6f267fcaa59..d5ae5efaccb 100644 --- a/core/src/utils/status-tap.ts +++ b/core/src/utils/status-tap.ts @@ -1,7 +1,7 @@ import { readTask, writeTask } from '@stencil/core'; import { findClosestIonContent, scrollToTop } from './content'; -import { componentOnReady } from './helpers'; +import { waitForComponent } from './helpers'; export const startStatusTap = () => { const win = window; @@ -15,7 +15,7 @@ export const startStatusTap = () => { } const contentEl = findClosestIonContent(el); if (contentEl) { - new Promise((resolve) => componentOnReady(contentEl, resolve)).then(() => { + waitForComponent(contentEl).then(() => { writeTask(async () => { /** * If scrolling and user taps status bar, From 76a32dd40fcaf3d7bc58dbe8342411496e7e456a Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Apr 2026 10:48:23 -0700 Subject: [PATCH 5/8] feat(styles): remove webkit-overflow-scrolling --- core/src/components/action-sheet/action-sheet.scss | 1 - core/src/components/alert/alert.ios.scss | 1 - core/src/components/alert/alert.scss | 1 - core/src/components/content/content.scss | 2 -- 4 files changed, 5 deletions(-) diff --git a/core/src/components/action-sheet/action-sheet.scss b/core/src/components/action-sheet/action-sheet.scss index 2a2f85bb456..495e8421015 100644 --- a/core/src/components/action-sheet/action-sheet.scss +++ b/core/src/components/action-sheet/action-sheet.scss @@ -143,7 +143,6 @@ flex-shrink: 2; overscroll-behavior-y: contain; overflow-y: auto; - -webkit-overflow-scrolling: touch; pointer-events: all; background: var(--background); diff --git a/core/src/components/alert/alert.ios.scss b/core/src/components/alert/alert.ios.scss index 714efc03baf..b31b2577b3a 100644 --- a/core/src/components/alert/alert.ios.scss +++ b/core/src/components/alert/alert.ios.scss @@ -148,7 +148,6 @@ border-top: $alert-ios-list-border-top; overflow-y: auto; - -webkit-overflow-scrolling: touch; } .alert-tappable { diff --git a/core/src/components/alert/alert.scss b/core/src/components/alert/alert.scss index 9948a4127a9..0382b6bb898 100644 --- a/core/src/components/alert/alert.scss +++ b/core/src/components/alert/alert.scss @@ -94,7 +94,6 @@ .alert-message, .alert-input-group { box-sizing: border-box; - -webkit-overflow-scrolling: touch; overflow-y: auto; overscroll-behavior-y: contain; } diff --git a/core/src/components/content/content.scss b/core/src/components/content/content.scss index 070b03d0ce5..d145cf705b0 100644 --- a/core/src/components/content/content.scss +++ b/core/src/components/content/content.scss @@ -82,8 +82,6 @@ .scroll-y, .scroll-x { - -webkit-overflow-scrolling: touch; - /** * This adds `.inner-scroll` as part of the * stacking context in WebKit. Without it, From 7646ae6c8c0ceab28c3f820176c85bd93a4564ac Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Apr 2026 11:08:14 -0700 Subject: [PATCH 6/8] feat(content): improvements --- core/src/components/content/content.scss | 26 ++-------- .../components/content/test/content.spec.ts | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/core/src/components/content/content.scss b/core/src/components/content/content.scss index d145cf705b0..a10716d90ed 100644 --- a/core/src/components/content/content.scss +++ b/core/src/components/content/content.scss @@ -208,6 +208,8 @@ // --------------------------------------------- .transition-effect { + @include mixins.position-horizontal(-100%); + display: none; position: absolute; @@ -219,18 +221,6 @@ pointer-events: none; } -:host(.content-ltr) .transition-effect { - /* stylelint-disable property-disallowed-list */ - left: -100%; - /* stylelint-enable property-disallowed-list */ -} - -:host(.content-rtl) .transition-effect { - /* stylelint-disable property-disallowed-list */ - right: -100%; - /* stylelint-enable property-disallowed-list */ -} - .transition-cover { position: absolute; @@ -247,6 +237,8 @@ } .transition-shadow { + @include mixins.position-horizontal(null, 0); + display: block; position: absolute; @@ -256,16 +248,6 @@ box-shadow: inset -9px 0 9px 0 rgba(0, 0, 100, 0.03); } -:host(.content-ltr) .transition-shadow { - /* stylelint-disable property-disallowed-list */ - right: 0; - /* stylelint-enable property-disallowed-list */ -} - :host(.content-rtl) .transition-shadow { - /* stylelint-disable property-disallowed-list */ - left: 0; - /* stylelint-enable property-disallowed-list */ - transform: scaleX(-1); } diff --git a/core/src/components/content/test/content.spec.ts b/core/src/components/content/test/content.spec.ts index 4dcf7f3e0a6..ee0f506ac0c 100644 --- a/core/src/components/content/test/content.spec.ts +++ b/core/src/components/content/test/content.spec.ts @@ -2,6 +2,54 @@ import { newSpecPage } from '@stencil/core/testing'; import { Content } from '../content'; +describe('content: transition shadow', () => { + it('should render transition effect elements in ios mode', async () => { + const page = await newSpecPage({ + components: [Content], + html: ``, + }); + + const content = page.body.querySelector('ion-content')!; + expect(content.shadowRoot!.querySelector('.transition-effect')).not.toBeNull(); + }); + + it('should not render transition effect elements in md mode', async () => { + const page = await newSpecPage({ + components: [Content], + html: ``, + }); + + const content = page.body.querySelector('ion-content')!; + expect(content.shadowRoot!.querySelector('.transition-effect')).toBeNull(); + }); +}); + +describe('content: element refs', () => { + it('getScrollElement should return the scroll element', async () => { + const page = await newSpecPage({ + components: [Content], + html: ``, + }); + + const content = page.body.querySelector('ion-content')!; + const scrollEl = await content.getScrollElement(); + + expect(scrollEl).toBe(content.shadowRoot!.querySelector('[part="scroll"]')); + }); + + it('getBackgroundElement should return the background element', async () => { + const page = await newSpecPage({ + components: [Content], + html: ``, + }); + + const content = page.body.querySelector('ion-content')!; + const backgroundEl = await content.getBackgroundElement(); + + expect(backgroundEl).toBe(content.shadowRoot!.querySelector('[part="background"]')); + }); +}); + describe('content: fixed slot placement', () => { it('should should fixed slot after content', async () => { const page = await newSpecPage({ From 412806fafb3727f73701e8e716c0641418427270 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Apr 2026 11:11:03 -0700 Subject: [PATCH 7/8] feat(contenet): remove transition theme --- core/src/components/content/content.interfaces.ts | 10 ---------- core/src/themes/ionic/default.tokens.ts | 11 +---------- core/src/themes/ios/default.tokens.ts | 9 --------- core/src/themes/md/default.tokens.ts | 9 --------- 4 files changed, 1 insertion(+), 38 deletions(-) diff --git a/core/src/components/content/content.interfaces.ts b/core/src/components/content/content.interfaces.ts index 2a14533cc16..7896dafac10 100644 --- a/core/src/components/content/content.interfaces.ts +++ b/core/src/components/content/content.interfaces.ts @@ -10,17 +10,7 @@ export interface IonContentRecipe { }; overflow?: string; - padding?: IonPadding; - - transition?: { - cover?: { - background?: string; - opacity?: string; - }; - - shadow?: string; - }; } export interface IonContentConfig {} diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index b068167673b..9bf0e0b1849 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -1,4 +1,4 @@ -import { rgba, currentColor, mix, dynamicFont } from '../../utils/theme'; +import { currentColor, mix, dynamicFont } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import { colors as baseColors } from '../base/shared.tokens'; import type { DefaultTheme } from '../themes.interfaces'; @@ -284,15 +284,6 @@ export const defaultTheme: DefaultTheme = { start: 'var(--ion-spacing-0)', top: 'var(--ion-spacing-0)', }, - - transition: { - cover: { - background: baseColors.black, - opacity: '0.1', - }, - - shadow: `inset -9px 0 9px 0 ${rgba('0, 0, 100', 0.03)}`, - }, }, IonItemDivider: { diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 72eaff3307d..3bfb7be3bcb 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -418,15 +418,6 @@ export const defaultTheme: DefaultTheme = { start: 'var(--ion-spacing-0)', top: 'var(--ion-spacing-0)', }, - - transition: { - cover: { - background: baseColors.black, - opacity: '0.1', - }, - - shadow: `inset -9px 0 9px 0 ${rgba('0, 0, 100', 0.03)}`, - }, }, IonItemDivider: { diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index 96d0c79d513..443592774f7 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -415,15 +415,6 @@ export const defaultTheme: DefaultTheme = { start: 'var(--ion-spacing-0)', top: 'var(--ion-spacing-0)', }, - - transition: { - cover: { - background: baseColors.black, - opacity: '0.1', - }, - - shadow: `inset -9px 0 9px 0 ${rgba('0, 0, 100', 0.03)}`, - }, }, IonItemDivider: { From c8f817c76f496e198bb3777d85e6edfd4c95bc56 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Apr 2026 11:33:58 -0700 Subject: [PATCH 8/8] refactor(contenet): update padding variables --- .../src/components/content/test/basic/index.html | 16 +++++++++++++++- .../content/test/fullscreen/index.html | 4 ++++ .../content/test/standalone/index.html | 6 +++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/core/src/components/content/test/basic/index.html b/core/src/components/content/test/basic/index.html index 663dd11ca4b..6aed6df44d5 100644 --- a/core/src/components/content/test/basic/index.html +++ b/core/src/components/content/test/basic/index.html @@ -26,7 +26,7 @@
- +

@@ -135,6 +135,20 @@ f:last-of-type { background: yellow; } + + #content { + --ion-content-padding-start: 16px; + --ion-content-padding-end: 16px; + --ion-content-padding-top: 16px; + --ion-content-padding-bottom: 16px; + + text-align: center; + flex: 2; + } + + ion-content#content-part::part(background) { + background: #eee; + } diff --git a/core/src/components/content/test/fullscreen/index.html b/core/src/components/content/test/fullscreen/index.html index 8fa6eddfbe9..23dc29439d0 100644 --- a/core/src/components/content/test/fullscreen/index.html +++ b/core/src/components/content/test/fullscreen/index.html @@ -67,6 +67,10 @@ ion-content { --ion-content-background: linear-gradient(90deg, blue, red); --ion-content-color: white; + --ion-content-padding-start: 16px; + --ion-content-padding-end: 16px; + --ion-content-padding-top: 16px; + --ion-content-padding-bottom: 16px; } p:first-child { diff --git a/core/src/components/content/test/standalone/index.html b/core/src/components/content/test/standalone/index.html index b492eb36ba7..acd5d4f94b5 100644 --- a/core/src/components/content/test/standalone/index.html +++ b/core/src/components/content/test/standalone/index.html @@ -15,7 +15,7 @@

- +

Heading

Heading

Heading

@@ -28,7 +28,7 @@
Heading
- +

Heading

Heading

Heading

@@ -41,7 +41,7 @@
Heading
- +

Heading

Heading

Heading