diff --git a/core/api.txt b/core/api.txt index a8217994eef..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 @@ -758,15 +757,14 @@ 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-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,part,background ion-content,part,scroll diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 5d1eff6638c..3b7f07e4dde 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"; @@ -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/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-interface.ts b/core/src/components/content/content.interfaces.ts similarity index 66% rename from core/src/components/content/content-interface.ts rename to core/src/components/content/content.interfaces.ts index c4e04431c2e..7896dafac10 100644 --- a/core/src/components/content/content-interface.ts +++ b/core/src/components/content/content.interfaces.ts @@ -1,4 +1,19 @@ import type { GestureDetail } from '../../interface'; +import type { IonPadding } from '../../themes/themes.interfaces'; + +export interface IonContentRecipe { + background?: string; + color?: string; + + font?: { + family?: string; + }; + + overflow?: string; + padding?: IonPadding; +} + +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 de81ebc89b4..a10716d90ed 100644 --- a/core/src/components/content/content.scss +++ b/core/src/components/content/content.scss @@ -1,34 +1,27 @@ -@import "../../themes/native/native.globals"; +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.color" as color; // Content // -------------------------------------------------- :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-font-family: Font family of the content * - * @prop --keyboard-offset: Keyboard offset of the content + * @prop --ion-content-overflow: Overflow behavior of the scrollable area * - * @prop --offset-top: Offset top of the content - * @prop --offset-bottom: Offset bottom 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 */ - --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; + --internal-keyboard-offset: 0px; + --internal-offset-top: 0px; + --internal-offset-bottom: 0px; display: block; position: relative; @@ -44,36 +37,26 @@ 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(--background); -} +// Content Inner Scroll +// --------------------------------------------- .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) + @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(--internal-keyboard-offset) + var(--internal-offset-bottom)), + var(--ion-content-padding-start) ); position: absolute; - color: var(--color); + color: var(--ion-content-color); box-sizing: border-box; @@ -89,10 +72,16 @@ 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; - /** * This adds `.inner-scroll` as part of the * stacking context in WebKit. Without it, @@ -115,15 +104,29 @@ } .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; } +// 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; @@ -142,6 +145,9 @@ top: -1px; } +// Content Sizing +// --------------------------------------------- + :host(.content-sizing) { display: flex; @@ -159,6 +165,7 @@ contain: none; } + :host(.content-sizing) .inner-scroll { position: relative; @@ -172,11 +179,37 @@ 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 { + @include mixins.position-horizontal(-100%); + display: none; position: absolute; @@ -188,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; @@ -216,6 +237,8 @@ } .transition-shadow { + @include mixins.position-horizontal(null, 0); + display: block; position: absolute; @@ -225,34 +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); } - -// 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 0c09660b9aa..fc84f4432e7 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -1,20 +1,19 @@ 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'; 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-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) { @@ -313,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!; } /** @@ -326,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!; } /** @@ -349,7 +347,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 +452,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 = theme === 'ios'; + /** + * 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 +466,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..6aed6df44d5 100644 --- a/core/src/components/content/test/basic/index.html +++ b/core/src/components/content/test/basic/index.html @@ -26,7 +26,7 @@
- +

@@ -136,6 +136,16 @@ 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/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({ diff --git a/core/src/components/content/test/fullscreen/index.html b/core/src/components/content/test/fullscreen/index.html index bf0cebade91..23dc29439d0 100644 --- a/core/src/components/content/test/fullscreen/index.html +++ b/core/src/components/content/test/fullscreen/index.html @@ -65,8 +65,12 @@ } ion-content { - --background: linear-gradient(90deg, blue, red); - --color: white; + --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 daed419034d..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

@@ -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/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.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/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/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/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 e4d4407e2b6..9bf0e0b1849 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -268,6 +268,24 @@ export const defaultTheme: DefaultTheme = { }, }, + IonContent: { + background: baseColors.backgroundColor, + color: baseColors.textColor, + + font: { + family: 'var(--ion-font-family, inherit)', + }, + + overflow: 'auto', + + padding: { + bottom: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + top: 'var(--ion-spacing-0)', + }, + }, + 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..3bfb7be3bcb 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -402,6 +402,24 @@ export const defaultTheme: DefaultTheme = { }, }, + IonContent: { + background: baseColors.backgroundColor, + color: baseColors.textColor, + + font: { + family: 'var(--ion-font-family, inherit)', + }, + + overflow: 'auto', + + padding: { + bottom: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + top: 'var(--ion-spacing-0)', + }, + }, + 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..443592774f7 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -399,6 +399,24 @@ export const defaultTheme: DefaultTheme = { }, }, + IonContent: { + background: baseColors.backgroundColor, + color: baseColors.textColor, + + font: { + family: 'var(--ion-font-family, inherit)', + }, + + overflow: 'auto', + + padding: { + bottom: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-0)', + top: 'var(--ion-spacing-0)', + }, + }, + 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; 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/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'); } } 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, 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 {