From 7afba1af359fea4deb78c612320e1291610bb007 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 24 Apr 2026 16:03:45 -0700 Subject: [PATCH] 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;