From 4e8ecc293a9b0655dc173df96609b4e7a4031d5a Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:59:14 -0400 Subject: [PATCH 01/18] fix(modal): prevent ion-content collapsing at content-based heights resolves #31149 --- core/src/components/content/content.tsx | 28 ++++++++++++++++++- core/src/components/modal/modal.scss | 15 +++++++++- core/src/css/core.scss | 37 +++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index b0673b414d5..485862d4749 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -310,6 +310,26 @@ export class Content implements ComponentInterface { return forceOverscroll === undefined ? mode === 'ios' && isPlatform('ios') : forceOverscroll; } + /** + * Whether this component should size itself to its contents height, which + * is the case inside any popover and inside a modal whose `--height` is a + * content-based value. Those overlays give the content no definite height + * to fill. + */ + private shouldSizeToContent() { + if (hostContext('ion-popover', this.el)) { + return true; + } + + const modal = this.el.closest('ion-modal'); + if (modal === null) { + return false; + } + + const height = getComputedStyle(modal).getPropertyValue('--height').trim(); + return CONTENT_SIZED_HEIGHTS.includes(height); + } + private resize() { /** * Only force update if the component is rendered in a browser context. @@ -538,7 +558,7 @@ export class Content implements ComponentInterface { class={createColorClasses(this.color, { [mode]: true, 'content-fullscreen': this.fullscreen, - 'content-sizing': hostContext('ion-popover', this.el), + 'content-sizing': this.shouldSizeToContent(), overscroll: forceOverscroll, [`content-${rtl}`]: true, })} @@ -579,6 +599,12 @@ export class Content implements ComponentInterface { } } +/** + * `ion-modal` `--height` values that size the modal to its contents, leaving + * children an indefinite height to resolve against. + */ +const CONTENT_SIZED_HEIGHTS = ['auto', 'fit-content', 'min-content', 'max-content']; + const getParentElement = (el: any) => { if (el.parentElement) { // normal element with a parent element diff --git a/core/src/components/modal/modal.scss b/core/src/components/modal/modal.scss index 0df4a448cd3..a55e456bb0f 100644 --- a/core/src/components/modal/modal.scss +++ b/core/src/components/modal/modal.scss @@ -27,7 +27,12 @@ --max-width: auto; --height: 100%; --min-height: auto; - --max-height: auto; + /** + * Clamps a content-sized `--height` (auto, fit-content, ...) to the + * overlay, giving the wrapper's flex children something to shrink + * toward so `ion-content` scrolls instead of overflowing. + */ + --max-height: 100%; --overflow: hidden; --border-radius: 0; --border-width: 0; @@ -87,8 +92,16 @@ ion-backdrop { /** * The wrapper receives programmatic focus for screen readers but should not * show a visible focus ring, which is meant only for keyboard navigation. + * + * A flex layout is required for the wrapper to size itself to its content + * when the modal is content-sized (`--height` is auto, fit-content, ...). + * This makes it so that the content can scroll when it overflows the wrapper. */ .modal-wrapper { + display: flex; + + flex-direction: column; + outline: none; } diff --git a/core/src/css/core.scss b/core/src/css/core.scss index c7f7357ab46..d86184a326a 100644 --- a/core/src/css/core.scss +++ b/core/src/css/core.scss @@ -203,9 +203,46 @@ ion-modal > .ion-page { contain: layout style; + /** + * Override the minimum height a flex item gets, which defaults to + * use the height of its own content. Without this, a modal sized + * to its content clips its overflow instead of scrolling it. + */ + min-height: 0; + height: 100%; } +/** + * Position the `ion-nav` and its page relatively when inside of an + * `ion-content` that is sized to its content. This allows the `ion-nav` + * to take its height from its page and size itself correctly. Without + * this, the modal will not appear as the nav will be 0 height. + */ +ion-content.content-sizing ion-nav, +ion-content.content-sizing ion-nav > .ion-page { + position: relative; + + contain: layout style; + + height: auto; +} + +/** + * Place every page in the same grid cell so they overlap, while still + * letting the nav take its height from the tallest of them. Without + * this, a transition that has two pages in the tree at once would + * render them one below the other. + */ +ion-content.content-sizing ion-nav { + display: grid; +} + +ion-content.content-sizing ion-nav > .ion-page { + grid-row: 1; + grid-column: 1; +} + .split-pane-visible > .ion-page.split-pane-main { position: relative; } From e63841c2c8b44b03715157b346991ccb9d2f0882 Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:43:53 -0400 Subject: [PATCH 02/18] test(modal): add content-height preview test --- .../modal/test/content-height/index.html | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 core/src/components/modal/test/content-height/index.html diff --git a/core/src/components/modal/test/content-height/index.html b/core/src/components/modal/test/content-height/index.html new file mode 100644 index 00000000000..6cc01769a64 --- /dev/null +++ b/core/src/components/modal/test/content-height/index.html @@ -0,0 +1,353 @@ + + +
+ +Short first page.
kn@PbrrhtvaIw18{a)oA^L*FL2wA3VUr*Zpm
zb+Jx~zhWLowmzLCJ8J}-GcA;G1^i*QIxvS&u-@Iz5ZN<) 8){xJcEe0_m<@Sn-RfKyrKZQCKs8^63X+m(l{={h
zWJughlX99%5sl|YG=o#*#g3#4k{b`jez>{Cq+#lX6coCBUqs`lhmPA0SS_->1JFz)
z^j7Z4-#bqk;9E|3=;Fh;GMY$eMc+Cw6*u4S;0vJ~Rry6SvHid3vRT(#HtG7IscaXE
z$&xJG>L`_QxQe;L>F4-9z212g`
#lCYH_2MMJEc|qNi1lQ!0-Cp3R&==#yl>tTeFQKKRehkes
zT4=~Sb%cGUkmu1_FqxN0z3!*Wknj`6vc1=xQKVX+@#y1tRPyLP(N(OD=K_eakF9Ck
zLvN3fD%^Vszt6?bgA79^H}*W!JIdNiWqbMReE(Bn>dwIxZGzA5x#&8A9q~z>a{klz
z$g%SJXkh2G`iJXgM2jb-hG2cF0#Ahvlt{2*J8azi`=Bh@N9q3fi6Ur~^~c-0B&`%p
zxUnspovL?-*fLev=j3x0
ztvn+=&(Wi?Y}qL?^U;4n2MKM0dJF$und8^p&9OXgY>CMu*P3gJQRt5Z?L@)j^3bv&
z+&9oTkYb6Dixr+#!lnpvGZ&o15c)UCh595D3<$w4TmIMM!n0<9O8@eWjY&N9lKqYS
z<5B+)m{OpbjXMnO8-ZADm)^O;Az{UtX7IrDf30U?Oi~C%6u8+x@pBsp3NnZmz!Xsl
z;!Jbl&sSdG27&iSmod!`)TVyzEr1Lha3|+{tGK?xv!sd_ziK~0#6vLkGtDd0ba6hX
z+ZRQqmfENjKJRe__A>jiRa(am0`V;dPPVvC1FpDbSdExW6Y}E3dFDgGj{s`O3vX?M
zM>3%lzz97COR@-D1Ly?<&@{S;sD2A+1
T57}
zHAcSoH0%IcsV}8LYgT}Mg<2s473!5zy;RmQtaB)k5{^B&)@GVqUHKwu_LjlaLT$ZO
zz#~GEQb6ljf=epJ$EhrL>%@$ZdT-q>No@p&A+
E?8#&SJL8wo0bl2cV3lW3pA|tyelC;{Wi-s)X#f%7$M|RSf?I(9tP`Ot8~I6??^zp;
z0}h=oFk2hY!G;tF@0|k1JJ9UXAwNks-9E_dlKBao{tiEBV3M1K{lP`$n45rqqxGh;
z*EoPU8N5NCjI=r1Qxf%N>~NK{jgf{UAd4EfRhaZK#1Hg
?TWvB@FeD_@T4;>)|mXL5;SWto>bQ5A-V
zrJtm|9wD-UNP{DnoK0idN11(oS^HmpeUN5xewtcx3XArRCBzo{=fy@elr*j72i19{
zgwOHtRIiA>qKti9uSl=h0^FDPyHY*n>@7qBFCUMmb{NM)8ibu7nvE#nbOJrE{O-(#Fqz)o7
zC2JH^nN!U0X6VlOfyp?uSeN$c%9j-LUN7*UAjP}Evs2{9oKyRE5F&c@qH<14A7j|VO3`>JVix*bK)klPDt%7Qw~_<_hEau{_m5P!eHu~T;=
z=W-!ersG=6OTp}#9Q2NtQV^1R&rA=B2o7}?&DxgZ&w1yFqN2L~(DrCyPTlBUWL#_G
zpIq3PK!1-k^O+B(+z*v*ZNV=`9!ES94m9P`xY!0|KAH3P&%1bKy`k;<&AWGWJ!Ce@
z6LH^;bnh;}NGkb6{-xq${!z3KhuhhuWG*A;waIL1Qy{AauAG{?=7m6!ooOY8L`2tb
zm5QOxvU5iL0}6gvVa`Uv1J`8zG`TIlwKiBh?WW1%!JsZ#(`KBusb_9?4GaBn(lZ=s
z3&s(L4jbf%%XT;*&f;r5SoDZ{(`8Pv