From dec46b5d317080dd5d97dc056f0d8e6d4c8c45ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20M=2E?= Date: Fri, 3 Apr 2026 19:24:33 +0100 Subject: [PATCH 01/14] fix(tab-button): update dark palette focused background color (#31050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue number: resolves resolves internal --------- ## What is the current behavior? In the dark palette, the `--ion-tab-bar-background-focused` CSS token was not defined. When a `ion-tab-button` receives focus, the `::after` overlay on `.button-native` falls back to `get-color-shade(#fff)` → `#e0e0e0` — a light grey that blends into the dark background and makes the focus indicator invisible. ## What is the new behavior? - `--ion-tab-bar-background-focused` is now defined in the dark palette: `#252525` for iOS and `#353535` for MD , providing a dark-appropriate focus indicator. The same token is added to `high-contrast-dark.scss` with the same values. - A screenshot e2e test is added under `tab-button/test/states/` (where the existing focused-state tests live) using `configs({ palettes: ['dark'] })` and `page.setContent()` with `class="ion-focused"` applied directly, matching the established pattern for focus-state testing in this component. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information The test triggers keyboard mode (required by `focus-visible.ts`) before focusing the inner `` element inside the tab button's shadow DOM, since calling `.focus()` on the host element is a no-op without `delegatesFocus`. - [Preview 1](https://ionic-framework-git-fw-6293-ionic1.vercel.app/src/components/tabs/test/basic?ionic:mode=ios&palette=dark) - [Preview 2](https://ionic-framework-git-fw-6293-ionic1.vercel.app/src/components/tabs/test/basic?ionic:mode=md&palette=dark) --------- Co-authored-by: ionitron Co-authored-by: Brandy Smith Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> --- .../tab-button/test/states/tab-button.e2e.ts | 22 ++++++++++++++++++ ...focus-ios-ltr-dark-Mobile-Chrome-linux.png | Bin 0 -> 1482 bytes ...ocus-ios-ltr-dark-Mobile-Firefox-linux.png | Bin 0 -> 1790 bytes ...focus-ios-ltr-dark-Mobile-Safari-linux.png | Bin 0 -> 1686 bytes ...-focus-md-ltr-dark-Mobile-Chrome-linux.png | Bin 0 -> 1305 bytes ...focus-md-ltr-dark-Mobile-Firefox-linux.png | Bin 0 -> 1538 bytes ...-focus-md-ltr-dark-Mobile-Safari-linux.png | Bin 0 -> 1432 bytes core/src/css/palettes/dark.scss | 2 ++ core/src/css/palettes/high-contrast-dark.scss | 2 ++ 9 files changed, 26 insertions(+) create mode 100644 core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Chrome-linux.png create mode 100644 core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Firefox-linux.png create mode 100644 core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Safari-linux.png create mode 100644 core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-md-ltr-dark-Mobile-Chrome-linux.png create mode 100644 core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-md-ltr-dark-Mobile-Firefox-linux.png create mode 100644 core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-md-ltr-dark-Mobile-Safari-linux.png diff --git a/core/src/components/tab-button/test/states/tab-button.e2e.ts b/core/src/components/tab-button/test/states/tab-button.e2e.ts index bd10cad844b..5af2a6f660b 100644 --- a/core/src/components/tab-button/test/states/tab-button.e2e.ts +++ b/core/src/components/tab-button/test/states/tab-button.e2e.ts @@ -80,3 +80,25 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c }); }); }); + +configs({ palettes: ['dark'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { + test.describe(title('tab-button: states in dark palette'), () => { + test.describe('focus', () => { + test('should render correct focus state in dark palette', async ({ page }) => { + await page.setContent( + ` + + + Favorites + + + `, + config + ); + + const tabBar = page.locator('ion-tab-bar'); + await expect(tabBar).toHaveScreenshot(screenshot('tab-button-focus')); + }); + }); + }); +}); diff --git a/core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Chrome-linux.png b/core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Chrome-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..14d96bb13658fd3abd2f2de83671402def9dfe3c GIT binary patch literal 1482 zcmai!c{~#e0LSO3C-H2Axk9{=Yx0;YOOD4JlL@6Va`c+bO>>RrPDO6gd_IIQ49zlS z9%YZpHH1A|jvVFVSab6{?~nIy@1Ni2`}uwU|7Ko6+KG$Givj=uafH1M3IGt$Ip$wQ z1dq33sbT^EIFX33xqKC0wlY2AsBlk_xxsy%oMjs4>nY{bi|Z@IsNC(zU;Y{hvlHrX z(P~g}TDqouX3IxnPgYzM4&3T$wiG?D8>s@O0QC*W*?KsK5y<>CBy@AUBZ^V%<=Jm0 z=SV=oq4Bq8p&G?BE}9nyB;fJI0DyVOGm|i4By(s8D1djn7|YLy{K*B7blyg!3w%!4 zM+_vWpC%#zwoU;&RJ8*10#yNi8Nj3qKi17~!4P$%Xs=z5f!*hc9`zMzq&%V;a?qWn zqsY_@WVP4X(v!ZYKx!C3SltxVeRgZlrryI?QIH8bw<5<;dtcF6`0-ox;z_0fzwyij z0h?qk(SISl^hzrTQ_IoI%R7Mo zaZwRXoG(fZY~V~Cd9SBcg5LE!rB};1KFdEb36)~yTDjW3kcK(H2EV6B&0BV9gjPU- zDEq1w1}P%W8bmKM)W*0rw2{z6Z-X)8P z$L`2#D}#%q)uQ!uABw|qa8;5-?*Iv;EUOGgGI+b)EBoeOJ^U^qOyRUpk9a)I-=xf# z;Je&ym+X?rT~>JDOgO8IeZhc{313=MceDL!2F2oRZ^Pr)?;IRp@peXnZslb^+Y1RL z^ANs^e;=kHT}cc2lyum1Q@a z*;%JSQPftYsPtu?L$v9w-v_UqH=~)Dg0!lQJT+w~StaF7MP898t>z5%p~aJFzEG9! zv$HX-a!+$5e zXesLu$#cqI!rQI~m7*@Z312LrIVe?@F)lNfWbPRnm75s59?p1_SZ>cyNJdQ49nU`z zGkrF1Bk-N8OPJyHN08ao*k-IEr`mC2o}cj{jA%@{C>!PuGh)R|ODhh{71^%(v}?dY zca`T@LWi~Q);gV2r}>I%B)W;zW?N!fGzTleRQuq*bIC{2V0>gexM(rAs0p%cT-{b{ zl+we642zG%bSona?K)p9;I))3@*IX8=Fjuh%*OLUevy uqW=lie|iOa9O9#+g*ai0V>tP5iiH5w1pWM_9+#lw2Y|3e+B91E5dQ&ex482F literal 0 HcmV?d00001 diff --git a/core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Firefox-linux.png b/core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..841928b47fabd10488053aaee8946bb7963fbe2e GIT binary patch literal 1790 zcmd6o{Xf%tAIHC2vu3R1W`s@~>lik2JGYs;Yje|<+?-v~q@^6>wqw#MqokuQH(57W zIGKo5?nUj;6wZYs>THjigzUJTn}cPD^ZgI5U$0+2kJk^+A71bG=k?ppg1V z002-vRIlRz03m?d1_1|8Vvb!N0Knh)c)16qGUX4Xe!(ZSv$Ovdn)1$~VZg$RS?+J< zYi+FFm)-BHjoSBk9-B5zx07;D0S%M@u(OrtzC z#rfpGW%LT^)ouwhIoGkn^kDL><}(yNpYM+_P|XKaQ#s_|om+qW;%_Wbh18*3LA!eE)(z2)V^B4_qf0aF-SdgQXx2G);iXN#>=0RJnNIT#;{lE6B84PA!H8<_4@;7c2KOI?zhB2a8Y(AuO(oQ zhglMde>#ry;WMsjtJLpYQZ?+mTT~v^#Yz*e&SlkMRC0^wY|N4>(>JjYYN3|b6KEhZBF8slG4I+X46=nH8Pnjbj4!;WfkzX{byx^BD^KG zCA+rQJ&(r}iRbh<-EvlahG>N3WF!lb*oVcaC}xYl(PRT}^%0&{=rG!z0ts=>nDZz)7YX5*b!mlszizx3o4 zwjfN6|H``VIDuc?D5vW(PYbQ ziR?pvsApyK9Fx!9*7Ka{yYz^{5fy(&QXn(Pha=oA3`lX@ni?}#_sXiO+8f6jlSYR^ zpC|l^ZVNa=(N~Ul?f%c~igML9I54n+zKp^1#BcYSMplcQ(pIa*eG8+)S`*T(n}s9b zD{Pw1T~{iV!$U(@xm;e{(B0i#gAcHjw*OR14yAq6hiZ;mdqTCv>X3%bbS=d91YSN8 ziEIkV(h53vuIhD9Pc0c5Zbr1Q5K9a#m@genHIRxIDe~bePSo_(dmaVQ>t+P1CJVEz zT$%dGuxcqc8(7Z}U;+@R#YDNZm7agagP}n@9x~|%%9=qnEH-$2%<+XitaB-gN{Pj3vAz>1aBSYbcV+=UP z2BwPxXEqox?Fbf0# zPy%PJV<4Id*+7-~Wk|Wx> z3MxQA4?~XF&*|3y-7IL7*Nu$#27rJ%DAmbl5uh73=<$Ep6&(_E0NV9lM|iea7w}&L Ne7tF1Pd!d${sU+KH;@1T literal 0 HcmV?d00001 diff --git a/core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Safari-linux.png b/core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-ios-ltr-dark-Mobile-Safari-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..86b46f9a31fa76252c5ba0992b24727b84501b46 GIT binary patch literal 1686 zcmb_dYgE!{6aQ;rE)tox$tK4w%gdX-YF>yG<<%%-MK(2)yd|4?SxVVZGbGo`5h2Ys zO)VE}^<8gic$Yv^H@uQsE{kMXW)ezNC~l=ey8rL{Wk2kEnCF>u=9y>CZ+?9`o3Rn7|9i#b8h9Acg8lsf-ULzw8k=c;_!GvUFpOha0ATb# zJkIw-I_K>(InjJ$*U+-2V2cZTsq918VQ#licW9hxOA0Q(q$nsn z{QK{+dwMVq;wvM4yRRQ31Gi%>9h==F@$KFW%Dy)Hy?b@~kbF7$HQh>HgD_c71yKln-NdNp!}> z#^yw5=o}Tjy1KkqB9Q=p$YgFF9)n`>Hy>v1+}R)8u%nga*nR(^Ci>+vdX9QC*%|di z_cNq7{l?^EkYP3e&(95Ojt)9`a%gM}1{$wlzdpwu96VB6Tbp%vcVko2fGWj|jq5de z3Wj9(eBE&3%5i(#{?boj&82!SCyB4ap4>47I}_j8(cywZ4QaL7i^JbjC<4ZsYm1Z( z$@L{jV&meNMMZFo)wb)$HUlD;MU{$7r&r|VLFjaPeM7?@E|IpnMBZg-$;MglE5Y|f zJ6kwMEBdoHtulO2C=~1H|09Jd?3x-If@GhmF$l@@lP9}NMqS+8a%VLQ;!+1Rmk5PI z6DX9u*N?eivD^9uv<37JFE^{zY9uE8r?Rr0&+ljChbCt|y*f2D<=D^&r&u)9*Y~U6 z$MMU{%D@lMNVt_Z9fw95{nFW4dSoZeoa-&O8RGWFA+iIqN= z#RV4506J5TK7amv&t+X1!S?WO(-(81d;)<0P_+uYc4dVAQ)pg7a`N!P$^kF0f>}-cg$u<5TT#{F z{F@aOPt&|mo}S-?g~_^vRmT_WWTfC=KQeg_h{&{jFvT-4I@+qHre`~VjFErsF$ zfj|gEqALcYw<|w#4wv0o0E5HVsmw27Mu|yDahERLO2=4qpIKFHEmI&k zB!2j?{h6fFJA)J%IrAZEG}hHJZB{RK@y24mmbfqLKFc#bI2_K{x_DYH4+se{Pfblt zNlVkNt;Abl(qpFOtg{Os{SW1ferpJ+ThwS4#(zEt{2`T2K7U^6ZMOqDDU}{^+`0D_ zL93G30U(J72M6W7%^e*#q*Cc@xW6Yij!ZTXxgd~8y%Vet%nWl$yBWAPEb64ljE!<( zFqr;ARI7zITKMo*E#9`I>fSws$Ym)VT$&dbh>{UXIPF)@+MU|_m8-m7Id%F9C{ zA_~ZeH*)^9p`oKgLqnOub-u4)R{Qhi4|Z9k3pVXzU)n*P(YHH!i@M_3wVA*bMvF;@CQ?m z88I0G}SBDPd0K1s^rDl%mc6L|$HA87BJ8%371X!$vL{p-?K3AXWEvGzM}~u6$*vECBqe#tJgEk*aKaUMka1@xt+iAJRJ?XJnv$^ z3g1xm>-?+Q1%gqfQn~HX!9Yn{eD;861^HhYpMM+1u>RtW6pQU^p&O7b9H&OoyM^1} OZUOK|h&Y~KOwL~jT{?0A literal 0 HcmV?d00001 diff --git a/core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-md-ltr-dark-Mobile-Chrome-linux.png b/core/src/components/tab-button/test/states/tab-button.e2e.ts-snapshots/tab-button-focus-md-ltr-dark-Mobile-Chrome-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..e12f984796d8456670a880440b63edfb62d9215a GIT binary patch literal 1305 zcmb`H{Xf$Q0LQ-`nl6M_y>6K1qO1oJI`Xh2iE5diSJLQgo)`d(r6Gub@XQ;ZCZ~~*47*uL?4LWT3;dl zPzwgckCh2h*x=P!Ssi=7RH-HhO)$YhLDs+!Cb8LeCg*m7`+{EW#~}MN4S=nP)qBmX z3@rl-fNsl;fZKnbP@iW5mrgkoRTbba1+PBUF~%gg$l}uFN@hK?@t4l$qmtKG5+Xdy zo71#Qh>}@Iz$PF3mZE`oBbiF_9;nXidVw(ltJtnF)g=w1Jxo45ut}sGX;H2!u}e6W zP; zESd1AwdM+jmtE)RSoddA*my@bKA8^wQv)8U-y%$!urO({gjkT0|famJK zS=HWy>9tzw2KFM=A<+ijC1|0;!9Cx@}3BkgDMldu098ItQl<&XbKwqx8GcT+g@K5f08p&OU48BoSe(2Zi`@ zk#x7&LX5gID-e{Opem^iqO^|q5;n`qkdb+1LWjYsHZO&7sDsxz$5_@x(A6Vgy2U6f zvelFI75l#U-v&dQ}4M;r!Hk4unh@(%XPRv_muz0{bY}%yGF@; zSXsR1md8|MhO~$py^hj<(akkJNqupr+5FgRj9Gsv68-ANhH`wv+?$P=!>-JQcMIVA zi#}6Wm&O=ap`pX3A8L=|<$3K?GzS+;6`bl^{UQ-1E%}k5_kZ z#R#65+iaZ2qWavPjN4yRQbRonk^=9LBVZ;{O z#poV}O)B~^d6IiWQoOsOLYTdrDtP^#8ElG*$AY4n7WUPey4sKY(%)!ZRb#&kyiR&n zLa&*``Fsr>Yg^AF&Pt_mBQLSr#tskT<7fwQMB-nNJFP0AdnEcDj1<<;^G2zxi%VQu znvAmkc%cgn{odZy`8Euu8h8peAdgnKrvA);ioKSWmvQsHJ5*@ShZM>knc3g!;)8S+ zNYj2)SDqgwBLu`I;VAqBj<{Fv7l}FnpMwO99=`}>%)<}`Lp`%jH-Ti}e|h@9(rB78 m006xn-f(?+MR1^RW-|gZh_7BuG?^r?5Flhf3c=q;$o~g?OTt{!-OJPW0)M;Q9S#lDjg28)M(NRZ6F(9+RVHOVqP-ept&2SkJup;(B zqG+k-1|DACm5UZVn(maFoxQlXw|9Tsf!5Ym6DupP-@kvSWM_Nl<>hVJv**wC?^A1; zfu1no4U3JHjg5<|i;|U=kht*qv$3V6<;9O54Q*{~CT!nse)PzZfaK)I%jY^@>jj1y zQ^bBoZ4;9zO3KQe)*Giz6_t{g7oRIo?q+Ig`sK?Pmv7&`O_@F0d+XM%M+8(=R00wb z6z<-;cj?U=owMi9ci(AiX_+v8zWmHtvo2k{DERf;w;)eXPF^0Kj`Sl(k526D>}=S) zZ(rOWUVC*jv#>Y&H*9bS4i0|wT}(`D%B)#ldU|?~KL6>tdp9=n@6QiA!O`*lz^9KN z7alp{l9iVymLMW38dzSwyw9W&b6+A`A9+m)1*P8>Vd_Urd=O)INeHa0dZHgBGM z=FAxlYwOu;?ChDfwXqFHe|*?cSx}&`p_;LK{?C1fi^|HDojK!^m6<8{#{u>ROVjtgQUx{d@fhPxJz{xq*@I+kEus|4kCp*R9htGB;OOR$hEP&`?{u``Wc@ zM`VTj^X_OGO7L)&+s&RmJ0vVjt>M@`}_M>)zpZbe^fa6;FG{hH*Q4qOt48ZiVzkSUhnZQ+s7`s9+);5uI-*UVYl+$ zIDM-{zkgR7&MOo@n(*<_yAH;OznARWx9|G>k1I|j-}Y`ldOjmR|Gz=Roy6Ffm!I% zbwBL7-mkByd1Ck}HRtoEPmB8c_zaDnJ$cgcF1@t$Yk$Jd-MfRMqNGesOggN!|8+T6 zRatG_w(Zi-pO&ob?A`x0KJ7R;{okJFU%r@FT3K~%+_Gs?P)Q}+%EISIJU)i=1C!^A zU%zJg`T4O3?U9j@S+QV2!>ZM*jNa%oXo+Sab1-4OwfEhV8h z{q=XP_&0mO>VJ*9UaZ>VG35n2tEucxA!ik{Uy4c6r>`wLTm0}Lce_=BY>{31eDOC) z_g~ICZg|h8x+Y;;hRLp$jt<5-^X+Q2zRO%|Id<$=k-y`U__JruOsVF|ZrXcCSU@mR zlhxa$XJJBThoOj$pkZ5+s?xE54K7C{xTbYTI3E-eOxB1{O5&kJ(YYdjhp_2)^PV5+ z6t<7)KmK@0&`Nu|e;bpJd#DKQssI14zhCvgeNu+z4Uw?a zKS>)sR)#PwSiHFT^YioTPwn{he}QONtCY0#@0ZhyfBmhqva;H6Ge>2zXIgqXCkxY- zZQDM5dg{#(qbFYd?ajlVpPx_hQk9aDy0Nd;y49)ild$H9eP+W!AEW8?Ae>2ZA(2ZW|niq z!EAPFNy(I@s*(t0e);}AI6U0Fw$}FV-@hg%CIK2EFW$aY72seH=yd6D zQ3AT+*6rJt)!%gL_RlY{IMd24&cW8qux0Dk!Y?lZzrDNLeg8fCg9DAsCZ?tjKURFG zu(6Qgd-eJ?W5MpbF}q4SpFK-!I=H~p&Y`^A+`Ug`;jUe?Vt1Dn{`&ILXpd7+9x(7$ zJU-t4`S0&?24Fx)%E&BPwW>?qf1ZJr71x2I-QqX5ft_FO$<@{2JpBC31+T7XR#jE)sQ#|UursDFdVAhqgMY8DuV1-#?a|j?jVvrU z4lHzTKapZ|XJ;{c!@hlXi!N$>e}7+HhVS#Yx6yJo6%E&4FI~R;`T2bgo76NTM73u6 z7}?wNAGp3go^i&kSxREvh6V-=XVVV9{`#myZ@=rkJ(bSs>FQ;>=YD;C-Ppv0MW?=H z&b)cs=9y3R66|zY^ZRE_-O*yMR;MdhuNMCNl**8mm9?h7yN-B8xf}!;X*~fc{V43k+t#m+cRg*7#JCC zTJ&+*^5rkzzI}RYYxbQTh0V+T=L>PQN=Qg7h|t-zWy_Jh)!!K^etb~e7}2wCogOg! zB&4OIw=S00RCdEG@6L+l*-aZZeDHoe<6XLYfY!vOujZ`Y*Nj-RXMUq@ f*@2@J`o|O<-F7v!zr+Jrx-xjW`njxgN@xNAeEXi5 literal 0 HcmV?d00001 diff --git a/core/src/css/palettes/dark.scss b/core/src/css/palettes/dark.scss index 0c5ef08dd93..18179f2e2e8 100644 --- a/core/src/css/palettes/dark.scss +++ b/core/src/css/palettes/dark.scss @@ -126,6 +126,7 @@ $colors: ( --ion-text-color-step-900: #1a1a1a; --ion-text-color-step-950: #0d0d0d; --ion-item-background: #000000; + --ion-tab-bar-background-focused: #252525; --ion-card-background: #1c1c1d; } @@ -183,6 +184,7 @@ $colors: ( --ion-item-background: #1e1e1e; --ion-toolbar-background: #1f1f1f; --ion-tab-bar-background: #1f1f1f; + --ion-tab-bar-background-focused: #353535; --ion-card-background: #1e1e1e; } } diff --git a/core/src/css/palettes/high-contrast-dark.scss b/core/src/css/palettes/high-contrast-dark.scss index e0f3b8aeb57..c5533a12405 100644 --- a/core/src/css/palettes/high-contrast-dark.scss +++ b/core/src/css/palettes/high-contrast-dark.scss @@ -119,6 +119,7 @@ $lightest-text-color: $text-color; --ion-text-color-rgb: #{color-to-rgb-list($text-color)}; --ion-item-background: #000000; --ion-card-background: #1c1c1d; + --ion-tab-bar-background-focused: #252525; /// Only the item borders should increase in contrast /// Borders for elements like toolbars should remain the same @@ -185,6 +186,7 @@ $lightest-text-color: $text-color; --ion-item-background: #1e1e1e; --ion-toolbar-background: #1f1f1f; --ion-tab-bar-background: #1f1f1f; + --ion-tab-bar-background-focused: #353535; --ion-card-background: #1e1e1e; /// Only the item borders should increase in contrast From 308aef569d8c6ebc3ad2186bca6969da8e4b2a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Louren=C3=A7o?= Date: Tue, 7 Apr 2026 08:36:37 +0000 Subject: [PATCH 02/14] fix(datetime): multiple month selected and flakiness display (#31053) Issue number: resolves internal --------- ## What is the current behavior? Currently, the Datepicker keeps all the months visited as selected instead of only the one that is really selected On iOS devices, the month picker sometimes doesn't appear since the `datepicker-ready` class is not added ## What is the new behavior? Only the selected month appears selected in the month picker On iOS devices, an additional validation is done to add the `datepicker-ready` class ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information The [Basic](https://ionic-framework-7el9b9l3f-ionic1.vercel.app/src/components/datetime/test/basic/?ionic:theme=ionic) test should be used to test the multiple-month selection case --- core/src/components/datetime/datetime.tsx | 17 +++++++-- .../datetime/test/basic/datetime.e2e.ts | 37 +++++++++++++++++++ .../picker-column/picker-column.tsx | 6 +-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index c591972fd8d..e73cd55e0b8 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -1086,6 +1086,9 @@ export class Datetime implements ComponentInterface { connectedCallback() { this.clearFocusVisible = startFocusVisible(this.el).destroy; + this.loadTimeout = setTimeout(() => { + this.ensureReadyIfVisible(); + }, 100); } disconnectedCallback() { @@ -1093,9 +1096,7 @@ export class Datetime implements ComponentInterface { this.clearFocusVisible(); this.clearFocusVisible = undefined; } - if (this.loadTimeout) { - clearTimeout(this.loadTimeout); - } + this.loadTimeoutCleanup(); } /** @@ -1146,6 +1147,13 @@ export class Datetime implements ComponentInterface { }); }; + private loadTimeoutCleanup = () => { + if (this.loadTimeout) { + clearTimeout(this.loadTimeout); + this.loadTimeout = undefined; + } + }; + componentDidLoad() { const { el, intersectionTrackerRef } = this; @@ -1193,7 +1201,10 @@ export class Datetime implements ComponentInterface { * we still initialize listeners and mark the component as ready. * * We schedule this after everything has had a chance to run. + * + * We also clean up the load timeout to ensure that we don't have multiple timeouts running. */ + this.loadTimeoutCleanup(); this.loadTimeout = setTimeout(() => { this.ensureReadyIfVisible(); }, 100); diff --git a/core/src/components/datetime/test/basic/datetime.e2e.ts b/core/src/components/datetime/test/basic/datetime.e2e.ts index 6104d0014cf..bbc6033374f 100644 --- a/core/src/components/datetime/test/basic/datetime.e2e.ts +++ b/core/src/components/datetime/test/basic/datetime.e2e.ts @@ -349,6 +349,43 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { }); }); +/** + * This behavior does not differ across + * modes/directions. + */ + +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('datetime: month picker selection'), () => { + test('datetime: month picker selection', async ({ page }) => { + await page.setContent( + ` + + `, + config + ); + + await page.locator('.datetime-ready').waitFor(); + + const nextMonthButton = page.locator('ion-datetime .calendar-next-prev ion-button').nth(1); + const monthYearButton = page.locator('ion-datetime .calendar-month-year'); + + await expect(monthYearButton).toHaveText(/May 2022/); + + await nextMonthButton.click(); + await expect(monthYearButton).toHaveText(/June 2022/); + + await nextMonthButton.click(); + await expect(monthYearButton).toHaveText(/July 2022/); + + await monthYearButton.click(); + await page.waitForChanges(); + + const selectedMonthOptions = page.locator('.month-column ion-picker-column-option.option-active'); + await expect(selectedMonthOptions).toHaveCount(1); + }); + }); +}); + /** * This behavior does not differ across * modes/directions. diff --git a/core/src/components/picker-column/picker-column.tsx b/core/src/components/picker-column/picker-column.tsx index 905ba8cf81e..53d6ca90d17 100644 --- a/core/src/components/picker-column/picker-column.tsx +++ b/core/src/components/picker-column/picker-column.tsx @@ -1,7 +1,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core'; import { doc } from '@utils/browser'; -import { getElementRoot, raf } from '@utils/helpers'; +import { raf } from '@utils/helpers'; import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from '@utils/native/haptic'; import { isPlatform } from '@utils/platform'; import { createColorClasses } from '@utils/theme'; @@ -122,9 +122,7 @@ export class PickerColumn implements ComponentInterface { * Because this initial call to scrollActiveItemIntoView has to fire before * the scroll listener is set up, we need to manage the active class manually. */ - const oldActive = getElementRoot(el).querySelector( - `.${PICKER_ITEM_ACTIVE_CLASS}` - ); + const oldActive = el.querySelector(`.${PICKER_ITEM_ACTIVE_CLASS}`); if (oldActive) { this.setPickerItemActiveState(oldActive, false); } From f4ac4459f8317bd5eeff7d4809f9cb0991c8efd9 Mon Sep 17 00:00:00 2001 From: OS-jacobbell <228905018+OS-jacobbell@users.noreply.github.com> Date: Tue, 14 Apr 2026 07:27:28 -0600 Subject: [PATCH 03/14] fix(checkbox): show labels after page navigation (#31062) Issue number: resolves #31052 --------- ## What is the current behavior? After a page navigation, ion-checkbox's `onslotchange` event fires before the element's `textContent` has been updated. It is called again after `textContent` becomes readable on Safari, but is not called again after the `textContent` becomes readable on Chrome and Firefox. ## What is the new behavior? - Uses `MutationObserver` instead of `onslotchange` and fires specifically on character data changes. This ensures `hasLabelContent` is up to date. - `MutationObserver` does not fire on load, so `hasLabelContent` is initialized in `connectedCallback` ## Does this introduce a breaking change? - [ ] Yes - [X] No --- core/src/components/checkbox/checkbox.tsx | 68 ++++++++++++----------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 8a25b9200d5..99cb13c474a 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -151,44 +151,54 @@ export class Checkbox implements ComponentInterface { connectedCallback() { const { el } = this; - // Watch for class changes to update validation state. if (Build.isBrowser && typeof MutationObserver !== 'undefined') { - this.validationObserver = new MutationObserver(() => { - const newIsInvalid = checkInvalidState(el); - if (this.isInvalid !== newIsInvalid) { - this.isInvalid = newIsInvalid; - /** - * Screen readers tend to announce changes - * to `aria-describedby` when the attribute - * is changed during a blur event for a - * native form control. - * However, the announcement can be spotty - * when using a non-native form control - * and `forceUpdate()`. - * This is due to `forceUpdate()` internally - * rescheduling the DOM update to a lower - * priority queue regardless if it's called - * inside a Promise or not, thus causing - * the screen reader to potentially miss the - * change. - * By using a State variable inside a Promise, - * it guarantees a re-render immediately at - * a higher priority. - */ - Promise.resolve().then(() => { - this.hintTextId = this.getHintTextId(); - }); + this.validationObserver = new MutationObserver((mutations) => { + // Watch for label content changes + if (mutations.some((mutation) => mutation.type === 'characterData' || mutation.type === 'childList')) { + this.hasLabelContent = this.el.textContent !== ''; + } + // Watch for class changes to update validation state. + if (mutations.some((mutation) => mutation.type === 'attributes' && mutation.target === el)) { + const newIsInvalid = checkInvalidState(el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + /** + * Screen readers tend to announce changes + * to `aria-describedby` when the attribute + * is changed during a blur event for a + * native form control. + * However, the announcement can be spotty + * when using a non-native form control + * and `forceUpdate()`. + * This is due to `forceUpdate()` internally + * rescheduling the DOM update to a lower + * priority queue regardless if it's called + * inside a Promise or not, thus causing + * the screen reader to potentially miss the + * change. + * By using a State variable inside a Promise, + * it guarantees a re-render immediately at + * a higher priority. + */ + Promise.resolve().then(() => { + this.hintTextId = this.getHintTextId(); + }); + } } }); this.validationObserver.observe(el, { attributes: true, attributeFilter: ['class'], + characterData: true, + childList: true, + subtree: true, }); } // Always set initial state this.isInvalid = checkInvalidState(el); + this.hasLabelContent = this.el.textContent !== ''; } componentWillLoad() { @@ -267,10 +277,6 @@ export class Checkbox implements ComponentInterface { ev.stopPropagation(); }; - private onSlotChange = () => { - this.hasLabelContent = this.el.textContent !== ''; - }; - private getHintTextId(): string | undefined { const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; @@ -387,7 +393,7 @@ export class Checkbox implements ComponentInterface { id={this.inputLabelId} onClick={this.onDivLabelClick} > - + {this.renderHintText()}
From b431b4c94450b1176fa4e97792861b66da88741c Mon Sep 17 00:00:00 2001 From: ionitron Date: Wed, 15 Apr 2026 18:12:24 +0000 Subject: [PATCH 04/14] v8.8.4 --- CHANGELOG.md | 13 +++++++++++++ core/CHANGELOG.md | 13 +++++++++++++ core/package-lock.json | 6 +++--- core/package.json | 2 +- lerna.json | 2 +- packages/angular-server/CHANGELOG.md | 8 ++++++++ packages/angular-server/package-lock.json | 8 ++++---- packages/angular-server/package.json | 4 ++-- packages/angular/CHANGELOG.md | 8 ++++++++ packages/angular/package-lock.json | 8 ++++---- packages/angular/package.json | 4 ++-- packages/docs/CHANGELOG.md | 8 ++++++++ packages/docs/package-lock.json | 6 +++--- packages/docs/package.json | 2 +- packages/react-router/CHANGELOG.md | 8 ++++++++ packages/react-router/package-lock.json | 8 ++++---- packages/react-router/package.json | 4 ++-- packages/react/CHANGELOG.md | 8 ++++++++ packages/react/package-lock.json | 8 ++++---- packages/react/package.json | 4 ++-- packages/vue-router/CHANGELOG.md | 8 ++++++++ packages/vue-router/package-lock.json | 8 ++++---- packages/vue-router/package.json | 4 ++-- packages/vue/CHANGELOG.md | 8 ++++++++ packages/vue/package-lock.json | 8 ++++---- packages/vue/package.json | 4 ++-- 26 files changed, 127 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fae4854106..84abbbee409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + + +### Bug Fixes + +* **checkbox:** show labels after page navigation ([#31062](https://github.com/ionic-team/ionic-framework/issues/31062)) ([f4ac445](https://github.com/ionic-team/ionic-framework/commit/f4ac4459f8317bd5eeff7d4809f9cb0991c8efd9)), closes [#31052](https://github.com/ionic-team/ionic-framework/issues/31052) +* **datetime:** multiple month selected and flakiness display ([#31053](https://github.com/ionic-team/ionic-framework/issues/31053)) ([308aef5](https://github.com/ionic-team/ionic-framework/commit/308aef569d8c6ebc3ad2186bca6969da8e4b2a8d)) +* **tab-button:** update dark palette focused background color ([#31050](https://github.com/ionic-team/ionic-framework/issues/31050)) ([dec46b5](https://github.com/ionic-team/ionic-framework/commit/dec46b5d317080dd5d97dc056f0d8e6d4c8c45ac)) + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 1fd2212efdf..ee45ffd629a 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + + +### Bug Fixes + +* **checkbox:** show labels after page navigation ([#31062](https://github.com/ionic-team/ionic-framework/issues/31062)) ([f4ac445](https://github.com/ionic-team/ionic-framework/commit/f4ac4459f8317bd5eeff7d4809f9cb0991c8efd9)), closes [#31052](https://github.com/ionic-team/ionic-framework/issues/31052) +* **datetime:** multiple month selected and flakiness display ([#31053](https://github.com/ionic-team/ionic-framework/issues/31053)) ([308aef5](https://github.com/ionic-team/ionic-framework/commit/308aef569d8c6ebc3ad2186bca6969da8e4b2a8d)) +* **tab-button:** update dark palette focused background color ([#31050](https://github.com/ionic-team/ionic-framework/issues/31050)) ([dec46b5](https://github.com/ionic-team/ionic-framework/commit/dec46b5d317080dd5d97dc056f0d8e6d4c8c45ac)) + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) diff --git a/core/package-lock.json b/core/package-lock.json index 01a06435345..8bef7f2f1f7 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/core", - "version": "8.8.3", + "version": "8.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/core", - "version": "8.8.3", + "version": "8.8.4", "license": "MIT", "dependencies": { "@stencil/core": "4.43.0", @@ -9829,4 +9829,4 @@ } } } -} +} \ No newline at end of file diff --git a/core/package.json b/core/package.json index 47566399094..4321a90686f 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "8.8.3", + "version": "8.8.4", "description": "Base components for Ionic", "engines": { "node": ">= 16" diff --git a/lerna.json b/lerna.json index e79ac7702c2..ba3e4cc2dcf 100644 --- a/lerna.json +++ b/lerna.json @@ -3,5 +3,5 @@ "core", "packages/*" ], - "version": "8.8.3" + "version": "8.8.4" } \ No newline at end of file diff --git a/packages/angular-server/CHANGELOG.md b/packages/angular-server/CHANGELOG.md index 68ecf941df3..e92a2268e2e 100644 --- a/packages/angular-server/CHANGELOG.md +++ b/packages/angular-server/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + +**Note:** Version bump only for package @ionic/angular-server + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) **Note:** Version bump only for package @ionic/angular-server diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index eaa09666b5b..62015e76228 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular-server", - "version": "8.8.3", + "version": "8.8.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/angular-server", - "version": "8.8.3", + "version": "8.8.4", "license": "MIT", "dependencies": { - "@ionic/core": "^8.8.3" + "@ionic/core": "^8.8.4" }, "devDependencies": { "@angular-eslint/eslint-plugin": "^16.0.0", @@ -11289,4 +11289,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/angular-server/package.json b/packages/angular-server/package.json index 6e7325b33e3..2af2f292161 100644 --- a/packages/angular-server/package.json +++ b/packages/angular-server/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular-server", - "version": "8.8.3", + "version": "8.8.4", "description": "Angular SSR Module for Ionic", "keywords": [ "ionic", @@ -62,6 +62,6 @@ }, "prettier": "@ionic/prettier-config", "dependencies": { - "@ionic/core": "^8.8.3" + "@ionic/core": "^8.8.4" } } diff --git a/packages/angular/CHANGELOG.md b/packages/angular/CHANGELOG.md index 66721281c95..0c73bb9366f 100644 --- a/packages/angular/CHANGELOG.md +++ b/packages/angular/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + +**Note:** Version bump only for package @ionic/angular + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) **Note:** Version bump only for package @ionic/angular diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index 90b33a3e6b3..b9c31ace5a7 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular", - "version": "8.8.3", + "version": "8.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/angular", - "version": "8.8.3", + "version": "8.8.4", "license": "MIT", "dependencies": { - "@ionic/core": "^8.8.3", + "@ionic/core": "^8.8.4", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" @@ -9095,4 +9095,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/angular/package.json b/packages/angular/package.json index 2211afd24b3..3785f542d0a 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular", - "version": "8.8.3", + "version": "8.8.4", "description": "Angular specific wrappers for @ionic/core", "keywords": [ "ionic", @@ -48,7 +48,7 @@ } }, "dependencies": { - "@ionic/core": "^8.8.3", + "@ionic/core": "^8.8.4", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" diff --git a/packages/docs/CHANGELOG.md b/packages/docs/CHANGELOG.md index 588ab851d3d..2a483528e35 100644 --- a/packages/docs/CHANGELOG.md +++ b/packages/docs/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + +**Note:** Version bump only for package @ionic/docs + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) **Note:** Version bump only for package @ionic/docs diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index 6e6bd26dc7a..158656a20f7 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -1,13 +1,13 @@ { "name": "@ionic/docs", - "version": "8.8.3", + "version": "8.8.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/docs", - "version": "8.8.3", + "version": "8.8.4", "license": "MIT" } } -} +} \ No newline at end of file diff --git a/packages/docs/package.json b/packages/docs/package.json index 91c1147613d..71eabacc599 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/docs", - "version": "8.8.3", + "version": "8.8.4", "description": "Pre-packaged API documentation for the Ionic docs.", "main": "core.json", "types": "core.d.ts", diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index d7185dd9b1d..4354d774ac4 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + +**Note:** Version bump only for package @ionic/react-router + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) **Note:** Version bump only for package @ionic/react-router diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index dc82458d0e3..94f944650a9 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react-router", - "version": "8.8.3", + "version": "8.8.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/react-router", - "version": "8.8.3", + "version": "8.8.4", "license": "MIT", "dependencies": { - "@ionic/react": "^8.8.3", + "@ionic/react": "^8.8.4", "tslib": "*" }, "devDependencies": { @@ -6847,4 +6847,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/packages/react-router/package.json b/packages/react-router/package.json index e77fef9f281..ed337f5b494 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react-router", - "version": "8.8.3", + "version": "8.8.4", "description": "React Router wrapper for @ionic/react", "keywords": [ "ionic", @@ -36,7 +36,7 @@ "dist/" ], "dependencies": { - "@ionic/react": "^8.8.3", + "@ionic/react": "^8.8.4", "tslib": "*" }, "peerDependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 0f8400c8511..dc8997adad5 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + +**Note:** Version bump only for package @ionic/react + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) **Note:** Version bump only for package @ionic/react diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index 3395ac5f3dc..821b953f957 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react", - "version": "8.8.3", + "version": "8.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/react", - "version": "8.8.3", + "version": "8.8.4", "license": "MIT", "dependencies": { - "@ionic/core": "^8.8.3", + "@ionic/core": "^8.8.4", "ionicons": "^8.0.13", "tslib": "*" }, @@ -11916,4 +11916,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/react/package.json b/packages/react/package.json index a08514a5476..d7253110c7e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react", - "version": "8.8.3", + "version": "8.8.4", "description": "React specific wrapper for @ionic/core", "keywords": [ "ionic", @@ -40,7 +40,7 @@ "css/" ], "dependencies": { - "@ionic/core": "^8.8.3", + "@ionic/core": "^8.8.4", "ionicons": "^8.0.13", "tslib": "*" }, diff --git a/packages/vue-router/CHANGELOG.md b/packages/vue-router/CHANGELOG.md index 00d1ab61982..ea8af11eccb 100644 --- a/packages/vue-router/CHANGELOG.md +++ b/packages/vue-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + +**Note:** Version bump only for package @ionic/vue-router + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) **Note:** Version bump only for package @ionic/vue-router diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index 9c7b1d57853..05b1320c660 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue-router", - "version": "8.8.3", + "version": "8.8.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/vue-router", - "version": "8.8.3", + "version": "8.8.4", "license": "MIT", "dependencies": { - "@ionic/vue": "^8.8.3" + "@ionic/vue": "^8.8.4" }, "devDependencies": { "@ionic/eslint-config": "^0.3.0", @@ -12994,4 +12994,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index 96fbba44ad2..b1ae2a0db94 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue-router", - "version": "8.8.3", + "version": "8.8.4", "description": "Vue Router integration for @ionic/vue", "scripts": { "test.spec": "jest", @@ -44,7 +44,7 @@ }, "homepage": "https://github.com/ionic-team/ionic-framework#readme", "dependencies": { - "@ionic/vue": "^8.8.3" + "@ionic/vue": "^8.8.4" }, "devDependencies": { "@ionic/eslint-config": "^0.3.0", diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index 425c7e49817..30c774c6c81 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.8.4](https://github.com/ionic-team/ionic-framework/compare/v8.8.3...v8.8.4) (2026-04-15) + +**Note:** Version bump only for package @ionic/vue + + + + + ## [8.8.3](https://github.com/ionic-team/ionic-framework/compare/v8.8.2...v8.8.3) (2026-04-01) **Note:** Version bump only for package @ionic/vue diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index a5c15bc6024..d1aba690bf3 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue", - "version": "8.8.3", + "version": "8.8.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/vue", - "version": "8.8.3", + "version": "8.8.4", "license": "MIT", "dependencies": { - "@ionic/core": "^8.8.3", + "@ionic/core": "^8.8.4", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" }, @@ -4022,4 +4022,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/packages/vue/package.json b/packages/vue/package.json index 372305c4b78..277719e7c3b 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue", - "version": "8.8.3", + "version": "8.8.4", "description": "Vue specific wrapper for @ionic/core", "scripts": { "eslint": "eslint src", @@ -68,7 +68,7 @@ "vue-router": "^4.0.16" }, "dependencies": { - "@ionic/core": "^8.8.3", + "@ionic/core": "^8.8.4", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" }, From 7053da1a5e803f9095c28e6f8c3c4215bd0aad20 Mon Sep 17 00:00:00 2001 From: ionitron Date: Wed, 15 Apr 2026 18:13:11 +0000 Subject: [PATCH 05/14] chore(): update package lock files --- core/package-lock.json | 2 +- packages/angular-server/package-lock.json | 14 +++++------ packages/angular/package-lock.json | 8 +++--- packages/docs/package-lock.json | 2 +- packages/react-router/package-lock.json | 30 +++++++++++------------ packages/react/package-lock.json | 8 +++--- packages/vue-router/package-lock.json | 30 +++++++++++------------ packages/vue/package-lock.json | 8 +++--- 8 files changed, 51 insertions(+), 51 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index 8bef7f2f1f7..007fed4a238 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -9829,4 +9829,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index 62015e76228..798964509f8 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1031,9 +1031,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "license": "MIT", "dependencies": { "@stencil/core": "4.43.0", @@ -7309,9 +7309,9 @@ "dev": true }, "@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "requires": { "@stencil/core": "4.43.0", "ionicons": "^8.0.13", @@ -11289,4 +11289,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index b9c31ace5a7..1178f97c7da 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1398,9 +1398,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "license": "MIT", "dependencies": { "@stencil/core": "4.43.0", @@ -9095,4 +9095,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index 158656a20f7..5110fe60ab6 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -10,4 +10,4 @@ "license": "MIT" } } -} \ No newline at end of file +} diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index 94f944650a9..37b215ff62c 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -238,9 +238,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "license": "MIT", "dependencies": { "@stencil/core": "4.43.0", @@ -418,12 +418,12 @@ } }, "node_modules/@ionic/react": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.8.3.tgz", - "integrity": "sha512-mqUftIoYROKSiqD9rOyF0XwR1/5dsGpAW/JD5bcPJqbNKx2bopb0/uiddeQ3Vbhi37tz5nsxzX0NB1+3cdx7hQ==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.8.4.tgz", + "integrity": "sha512-qlQNV6sRVIsq+vs33COKfzvMbNKRpPBGWYATv6vsNWmX7OhOm9tk7QSZl91LECvZRHMIe6Sqn9xMB+Aw/IBRfA==", "license": "MIT", "dependencies": { - "@ionic/core": "8.8.3", + "@ionic/core": "8.8.4", "ionicons": "^8.0.13", "tslib": "*" }, @@ -4178,9 +4178,9 @@ "dev": true }, "@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "requires": { "@stencil/core": "4.43.0", "ionicons": "^8.0.13", @@ -4284,11 +4284,11 @@ "requires": {} }, "@ionic/react": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.8.3.tgz", - "integrity": "sha512-mqUftIoYROKSiqD9rOyF0XwR1/5dsGpAW/JD5bcPJqbNKx2bopb0/uiddeQ3Vbhi37tz5nsxzX0NB1+3cdx7hQ==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.8.4.tgz", + "integrity": "sha512-qlQNV6sRVIsq+vs33COKfzvMbNKRpPBGWYATv6vsNWmX7OhOm9tk7QSZl91LECvZRHMIe6Sqn9xMB+Aw/IBRfA==", "requires": { - "@ionic/core": "8.8.3", + "@ionic/core": "8.8.4", "ionicons": "^8.0.13", "tslib": "*" } @@ -6847,4 +6847,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index 821b953f957..487b165f5b4 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -736,9 +736,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "license": "MIT", "dependencies": { "@stencil/core": "4.43.0", @@ -11916,4 +11916,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index 05b1320c660..0dffc9612f6 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -673,9 +673,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "license": "MIT", "dependencies": { "@stencil/core": "4.43.0", @@ -868,12 +868,12 @@ } }, "node_modules/@ionic/vue": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.8.3.tgz", - "integrity": "sha512-6bwi2RodoxECt2Ef6C6km4ZkuH4TweyhF800/XpunOKjM2Iya3pD1j230KSRd35cJpAenGc1uplLdIZhAyMsWg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.8.4.tgz", + "integrity": "sha512-FZftTMul3zw7vjZHAxznheSu0sy0h6GSLtKNeU5U4XtziOaMYZ9dLP5TWsBjRVahAEtqn+n6493fYW4EPE+dvw==", "license": "MIT", "dependencies": { - "@ionic/core": "8.8.3", + "@ionic/core": "8.8.4", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" } @@ -8044,9 +8044,9 @@ "dev": true }, "@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "requires": { "@stencil/core": "4.43.0", "ionicons": "^8.0.13", @@ -8159,11 +8159,11 @@ "requires": {} }, "@ionic/vue": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.8.3.tgz", - "integrity": "sha512-6bwi2RodoxECt2Ef6C6km4ZkuH4TweyhF800/XpunOKjM2Iya3pD1j230KSRd35cJpAenGc1uplLdIZhAyMsWg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.8.4.tgz", + "integrity": "sha512-FZftTMul3zw7vjZHAxznheSu0sy0h6GSLtKNeU5U4XtziOaMYZ9dLP5TWsBjRVahAEtqn+n6493fYW4EPE+dvw==", "requires": { - "@ionic/core": "8.8.3", + "@ionic/core": "8.8.4", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" } @@ -12994,4 +12994,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index d1aba690bf3..b21f4c5a4ba 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -222,9 +222,9 @@ "dev": true }, "node_modules/@ionic/core": { - "version": "8.8.3", - "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.3.tgz", - "integrity": "sha512-qvl+bRgZRvAJ35eW2iW0Vlo11T/EQsPazMU6z45QxJvcLukGJ59MwubjDTx6dPKteful4/FBzVt9etCvcNp8Gg==", + "version": "8.8.4", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.8.4.tgz", + "integrity": "sha512-RWts/72xtNNJDZQtntMRxB68KDctq76INV5OmLWWc0rlgcxOlNQDNH+lTBH7kV9vQ78JDVGSgi10ax3oQ3dIIQ==", "license": "MIT", "dependencies": { "@stencil/core": "4.43.0", @@ -4022,4 +4022,4 @@ "dev": true } } -} \ No newline at end of file +} From 12800cabfd22f785fcadb892fbbc91b5eff92dcd Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 15 Apr 2026 13:30:10 -0700 Subject: [PATCH 06/14] test(datetime): skip failing test --- core/src/components/datetime/test/basic/datetime.e2e.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/components/datetime/test/basic/datetime.e2e.ts b/core/src/components/datetime/test/basic/datetime.e2e.ts index bbc6033374f..4865e243092 100644 --- a/core/src/components/datetime/test/basic/datetime.e2e.ts +++ b/core/src/components/datetime/test/basic/datetime.e2e.ts @@ -440,7 +440,10 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => */ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { test.describe(title('datetime: IO fallback'), () => { - test('should become ready even if IntersectionObserver never reports visible', async ({ page }, testInfo) => { + test('should become ready even if IntersectionObserver never reports visible', async ({ page, skip }, testInfo) => { + // TODO(FW-7284): Re-enable on WebKit after determining why it fails + skip.browser('webkit', 'Wheel is not available in WebKit'); + testInfo.annotations.push({ type: 'issue', description: 'https://github.com/ionic-team/ionic-framework/issues/30706', From 0c178ef7b3513744a461bbabd8dbcfcae008ba3f Mon Sep 17 00:00:00 2001 From: Shane Date: Thu, 16 Apr 2026 08:32:36 -0700 Subject: [PATCH 07/14] chore(build): add Vercel preview builds for framework test apps (#31073) Issue number: resolves internal --------- ## What is the current behavior? Vercel preview deployments only build and serve core Stencil component test pages. Framework-specific test apps (Angular, React, Vue, React Router) are not included, requiring developers to pull and build locally to validate them. ## What is the new behavior? Vercel preview deployments build and serve framework test apps alongside core component tests under a single preview URL. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information --- core/scripts/vercel-build.sh | 354 ++++++++++++++++++ core/vercel.json | 12 + packages/react/test/base/src/App.tsx | 3 +- .../react/test/base/src/react-app-env.d.ts | 8 + 4 files changed, 376 insertions(+), 1 deletion(-) create mode 100755 core/scripts/vercel-build.sh create mode 100644 core/vercel.json diff --git a/core/scripts/vercel-build.sh b/core/scripts/vercel-build.sh new file mode 100755 index 00000000000..daf44142dce --- /dev/null +++ b/core/scripts/vercel-build.sh @@ -0,0 +1,354 @@ +#!/bin/bash +# +# Vercel preview build script +# +# Builds core component tests (same as before) plus framework test apps +# (Angular, React, Vue) so they're all accessible from a single preview URL. +# +# Core tests: /src/components/{name}/test/{scenario} +# Angular test app: /angular/ +# React test app: /react/ +# Vue test app: /vue/ +# +set -e + +# Vercel places core/ at /vercel/path1 (bind mount). The full repo clone +# lives at /vercel/path0. We can't rely on `..` to reach it, so we search. +CORE_DIR=$(pwd) +OUTPUT_DIR="${CORE_DIR}/../_vercel_output" + +# Find the actual repo root (the directory containing packages/) +REPO_ROOT="" +for candidate in /vercel/path0 /vercel/path1 "${CORE_DIR}/.."; do + if [ -d "${candidate}/packages" ]; then + REPO_ROOT="${candidate}" + break + fi +done + +echo "=== Ionic Framework Preview Build ===" +echo "Core dir: ${CORE_DIR}" +echo "Repo root: ${REPO_ROOT:-NOT FOUND}" +if [ -z "${REPO_ROOT}" ]; then + echo "(This is expected in some Vercel configs -- framework test apps will be skipped)" +fi + +rm -rf "${OUTPUT_DIR}" +mkdir -p "${OUTPUT_DIR}" + +# Step 1 - Build Core (dependencies already installed by Vercel installCommand) +echo "" +echo "--- Step 1: Building Core ---" +npm run build + +# Copy core files to output. The test HTML files use relative paths like +# ../../../../../dist/ionic/ionic.esm.js so the directory structure must +# be preserved exactly. +echo "Copying core output..." +cp -r "${CORE_DIR}/src" "${OUTPUT_DIR}/src" +cp -r "${CORE_DIR}/dist" "${OUTPUT_DIR}/dist" +cp -r "${CORE_DIR}/css" "${OUTPUT_DIR}/css" +mkdir -p "${OUTPUT_DIR}/scripts" +cp -r "${CORE_DIR}/scripts/testing" "${OUTPUT_DIR}/scripts/testing" + +# Generate directory index pages so users can browse core test pages. +# Creates an index.html in every directory under src/components/ that +# doesn't already have one. Only includes child directories that eventually +# lead to a test page (an index.html). Prunes snapshot dirs and dead ends. +echo "Generating directory indexes for core tests..." +generate_dir_index() { + local dir="$1" + local url_path="$2" + # Skip if an index.html already exists (it's an actual test page) + [ -f "${dir}/index.html" ] && return + + local entries="" + for child in "${dir}"/*/; do + [ -d "${child}" ] || continue + local name=$(basename "${child}") + # Skip snapshot directories and hidden dirs + case "${name}" in *-snapshots|.*) continue ;; esac + # Only include if there's at least one index.html somewhere underneath + find "${child}" -name "index.html" -print -quit | grep -q . || continue + entries="${entries}${name}/\n" + done + + [ -z "${entries}" ] && return + + cat > "${dir}/index.html" << IDXEOF + + + + + + Index of ${url_path} + + + +

Index of ${url_path}

+ ../ +$(echo -e "${entries}") + + +IDXEOF +} + +# Walk all directories under src/ (bottom-up so parent indexes reflect pruned children) +find "${OUTPUT_DIR}/src" -depth -type d | while IFS= read -r dir; do + url_path="${dir#${OUTPUT_DIR}}" + generate_dir_index "${dir}" "${url_path}/" +done + +# Vercel mounts core/ at a separate path (path1) from the repo clone (path0). +# Framework packages reference core via relative paths (../../core/css etc.), +# which resolve to path0/core/ -- not path1/ where we just built. +# Symlink path0/core -> path1 so those references find the build outputs. +if [ -n "${REPO_ROOT}" ] && [ "${CORE_DIR}" != "${REPO_ROOT}/core" ] && [ -d "${REPO_ROOT}/core" ]; then + echo "Linking ${REPO_ROOT}/core -> ${CORE_DIR} (so framework builds find core outputs)" + rm -rf "${REPO_ROOT}/core" + ln -s "${CORE_DIR}" "${REPO_ROOT}/core" +fi + +# Check if the full repo is available +if [ -z "${REPO_ROOT}" ]; then + echo "" + echo "WARNING: Could not find repo root (no directory with packages/ found)" + echo "Only core tests will be deployed (framework test apps require the full repo)." + + # Generate landing page and exit -- core tests are still useful + cat > "${OUTPUT_DIR}/index.html" << 'LANDING_EOF' + +Ionic Framework - Preview +

Ionic Framework Preview

Core tests only. Browse to /src/components/{name}/test/{scenario}/

+ +LANDING_EOF + + echo "=== Preview build complete (core only) ===" + exit 0 +fi + +# Step 2 - Build Framework Packages (parallel) +echo "" +echo "--- Step 2: Building Framework Packages ---" + +build_angular_pkgs() { + (cd "${REPO_ROOT}/packages/angular" && npm install && npm run sync && npm run build) || return 1 + (cd "${REPO_ROOT}/packages/angular-server" && npm install && npm run build) || return 1 +} + +build_react_pkgs() { + (cd "${REPO_ROOT}/packages/react" && npm install && npm run sync && npm run build) || return 1 + (cd "${REPO_ROOT}/packages/react-router" && npm install && npm run build) || return 1 +} + +build_vue_pkgs() { + (cd "${REPO_ROOT}/packages/vue" && npm install && npm run sync && npm run build) || return 1 + (cd "${REPO_ROOT}/packages/vue-router" && npm install && npm run build) || return 1 +} + +build_angular_pkgs > /tmp/vercel-angular-pkg.log 2>&1 & +PID_ANG=$! +build_react_pkgs > /tmp/vercel-react-pkg.log 2>&1 & +PID_REACT=$! +build_vue_pkgs > /tmp/vercel-vue-pkg.log 2>&1 & +PID_VUE=$! + +ANG_PKG_OK=true; REACT_PKG_OK=true; VUE_PKG_OK=true +wait $PID_ANG || { echo "Angular packages failed:"; tail -30 /tmp/vercel-angular-pkg.log; ANG_PKG_OK=false; } +wait $PID_REACT || { echo "React packages failed:"; tail -30 /tmp/vercel-react-pkg.log; REACT_PKG_OK=false; } +wait $PID_VUE || { echo "Vue packages failed:"; tail -30 /tmp/vercel-vue-pkg.log; VUE_PKG_OK=false; } + +if ! $ANG_PKG_OK || ! $REACT_PKG_OK || ! $VUE_PKG_OK; then + echo "ERROR: Some framework package builds failed." + echo "Core tests will still be deployed. Skipping failed framework test apps." +else + echo "All framework packages built." +fi + +# Step 3 - Build Framework Test Apps (parallel) +echo "" +echo "--- Step 3: Building Framework Test Apps ---" + +# Find the best available app version for a given package. +# Scans the apps/ directory and picks the newest version (reverse version sort). +pick_app() { + local apps_dir="$1/apps" + [ -d "${apps_dir}" ] || return 1 + local app + app=$(ls -1d "${apps_dir}"/*/ 2>/dev/null | xargs -n1 basename | sort -V -r | head -1) + [ -n "${app}" ] && echo "${app}" && return 0 + return 1 +} + +build_angular_test() { + local APP + APP=$(pick_app "${REPO_ROOT}/packages/angular/test") || { + echo "[angular] No test app found, skipping." + return 0 + } + echo "[angular] Building ${APP}..." + + cd "${REPO_ROOT}/packages/angular/test" + ./build.sh "${APP}" + cd "build/${APP}" + npm install + npm run sync + # --base-href sets so Angular Router works under the sub-path + npm run build -- --base-href /angular/ + + # Output path assumes the 'browser' builder. If migrated to 'application' builder, update this. + if [ ! -d "dist/test-app/browser" ]; then + echo "[angular] ERROR: Expected output at dist/test-app/browser/ not found." + return 1 + fi + mkdir -p "${OUTPUT_DIR}/angular" + cp -r dist/test-app/browser/* "${OUTPUT_DIR}/angular/" + echo "[angular] Done." +} + +build_react_test() { + local APP + APP=$(pick_app "${REPO_ROOT}/packages/react/test") || { + echo "[react] No test app found, skipping." + return 0 + } + echo "[react] Building ${APP}..." + + cd "${REPO_ROOT}/packages/react/test" + ./build.sh "${APP}" + cd "build/${APP}" + npm install + npm run sync + # --base sets Vite's base URL; import.meta.env.BASE_URL is read by IonReactRouter basename + npx vite build --base /react/ + + mkdir -p "${OUTPUT_DIR}/react" + cp -r dist/* "${OUTPUT_DIR}/react/" + echo "[react] Done." +} + +build_vue_test() { + local APP + APP=$(pick_app "${REPO_ROOT}/packages/vue/test") || { + echo "[vue] No test app found, skipping." + return 0 + } + echo "[vue] Building ${APP}..." + + cd "${REPO_ROOT}/packages/vue/test" + ./build.sh "${APP}" + cd "build/${APP}" + npm install + npm run sync + # Vue Router already reads import.meta.env.BASE_URL which Vite sets from --base + npx vite build --base /vue/ + + mkdir -p "${OUTPUT_DIR}/vue" + cp -r dist/* "${OUTPUT_DIR}/vue/" + echo "[vue] Done." +} + +# TODO: Add build_react_router_test() when reactrouter6-* apps are added to +# packages/react-router/test/apps/ + +TEST_FAILED="" + +if $ANG_PKG_OK; then + build_angular_test > /tmp/vercel-angular-test.log 2>&1 & + PID_ANG_TEST=$! +fi +if $REACT_PKG_OK; then + build_react_test > /tmp/vercel-react-test.log 2>&1 & + PID_REACT_TEST=$! +fi +if $VUE_PKG_OK; then + build_vue_test > /tmp/vercel-vue-test.log 2>&1 & + PID_VUE_TEST=$! +fi + +if $ANG_PKG_OK; then + wait $PID_ANG_TEST || { echo "Angular test app failed:"; tail -30 /tmp/vercel-angular-test.log; TEST_FAILED="${TEST_FAILED} angular"; } +fi +if $REACT_PKG_OK; then + wait $PID_REACT_TEST || { echo "React test app failed:"; tail -30 /tmp/vercel-react-test.log; TEST_FAILED="${TEST_FAILED} react"; } +fi +if $VUE_PKG_OK; then + wait $PID_VUE_TEST || { echo "Vue test app failed:"; tail -30 /tmp/vercel-vue-test.log; TEST_FAILED="${TEST_FAILED} vue"; } +fi + +if [ -n "${TEST_FAILED}" ]; then + echo "" + echo "WARNING: Some test app builds failed:${TEST_FAILED}" + echo "Core tests and successful framework apps will still be deployed." +fi + +# Step 4 - Landing Page +echo "" +echo "--- Step 4: Generating landing page ---" + +cat > "${OUTPUT_DIR}/index.html" << 'LANDING_EOF' + + + + + + Ionic Framework - Preview + + + + + + +LANDING_EOF + +echo "" +echo "=== Preview build complete ===" +ls -la "${OUTPUT_DIR}" diff --git a/core/vercel.json b/core/vercel.json new file mode 100644 index 00000000000..64b88751822 --- /dev/null +++ b/core/vercel.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": null, + "installCommand": "npm install", + "buildCommand": "bash scripts/vercel-build.sh", + "outputDirectory": "../_vercel_output", + "rewrites": [ + { "source": "/angular/:path*", "destination": "/angular/index.html" }, + { "source": "/react/:path*", "destination": "/react/index.html" }, + { "source": "/vue/:path*", "destination": "/vue/index.html" } + ] +} diff --git a/packages/react/test/base/src/App.tsx b/packages/react/test/base/src/App.tsx index c8ea117f60e..5878f0214f6 100644 --- a/packages/react/test/base/src/App.tsx +++ b/packages/react/test/base/src/App.tsx @@ -44,7 +44,8 @@ setupIonicReact(); const App: React.FC = () => ( - + {/* Vercel previews serve this app under /react/, so derive basename from Vite's base URL */} + diff --git a/packages/react/test/base/src/react-app-env.d.ts b/packages/react/test/base/src/react-app-env.d.ts index 6431bc5fc6b..b4ef8effc8f 100644 --- a/packages/react/test/base/src/react-app-env.d.ts +++ b/packages/react/test/base/src/react-app-env.d.ts @@ -1 +1,9 @@ /// + +interface ImportMetaEnv { + readonly BASE_URL?: string; +} + +interface ImportMeta { + readonly env?: ImportMetaEnv; +} From 164af7ae3e516e375571c5d3589e0648a3a8ec5f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:40:18 -0400 Subject: [PATCH 08/14] chore(deps): update capacitor to v8.3.1 (#31075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [@capacitor/core](https://capacitorjs.com) ([source](https://redirect.github.com/ionic-team/capacitor)) | [`8.3.0` → `8.3.1`](https://renovatebot.com/diffs/npm/@capacitor%2fcore/8.3.0/8.3.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fcore/8.3.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fcore/8.3.0/8.3.1?slim=true) | | [@capacitor/keyboard](https://redirect.github.com/ionic-team/capacitor-keyboard) | [`8.0.2` → `8.0.3`](https://renovatebot.com/diffs/npm/@capacitor%2fkeyboard/8.0.2/8.0.3) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fkeyboard/8.0.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fkeyboard/8.0.2/8.0.3?slim=true) | --- ### Release Notes
ionic-team/capacitor (@​capacitor/core) ### [`v8.3.1`](https://redirect.github.com/ionic-team/capacitor/blob/HEAD/CHANGELOG.md#831-2026-04-16) [Compare Source](https://redirect.github.com/ionic-team/capacitor/compare/8.3.0...8.3.1) ##### Bug Fixes - **android:** handle null versionName in isNewBinary() ([#​8397](https://redirect.github.com/ionic-team/capacitor/issues/8397)) ([aa1a660](https://redirect.github.com/ionic-team/capacitor/commit/aa1a660f364f9b5f5a1e350e279c8864b04dd13b)) - boundary value extraction for form-data requests ([#​7518](https://redirect.github.com/ionic-team/capacitor/issues/7518)) ([64ab854](https://redirect.github.com/ionic-team/capacitor/commit/64ab854c12330804c24275d88d3a9f7c8e52a73d)) - **cli:** check CAPACITOR\_COCOAPODS\_PATH in determinePackageManager ([#​8407](https://redirect.github.com/ionic-team/capacitor/issues/8407)) ([acb64ab](https://redirect.github.com/ionic-team/capacitor/commit/acb64ab92a37ff53701cde453558e272e2e11eb6)) - **system-bars:** use separate current styles ([#​8409](https://redirect.github.com/ionic-team/capacitor/issues/8409)) ([3d1f8d1](https://redirect.github.com/ionic-team/capacitor/commit/3d1f8d1b61480187375f5cd4de7ba999db007542))
ionic-team/capacitor-keyboard (@​capacitor/keyboard) ### [`v8.0.3`](https://redirect.github.com/ionic-team/capacitor-keyboard/blob/HEAD/CHANGELOG.md#803-2026-04-10) [Compare Source](https://redirect.github.com/ionic-team/capacitor-keyboard/compare/v8.0.2...v8.0.3) ##### Bug Fixes - **android:** fixing Keyboard interaction with SystemBars ([#​62](https://redirect.github.com/ionic-team/capacitor-keyboard/issues/62)) ([4afd89b](https://redirect.github.com/ionic-team/capacitor-keyboard/commit/4afd89b4af63609f40e970e7775fd03c5f6b407c))
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "every weekday before 11am" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/ionic-team/ionic-framework). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- core/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index 007fed4a238..70157701050 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -629,9 +629,9 @@ "license": "MIT" }, "node_modules/@capacitor/core": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.0.tgz", - "integrity": "sha512-S4ajn4G/fS3VJj8salxqH/3LO5PPWv1VxGKQ27OCajnDcLJjEg9VXwgMPnlypgkIOqCJ2fmQLtk8GT+BlI9/rw==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz", + "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==", "dev": true, "license": "MIT", "dependencies": { @@ -649,9 +649,9 @@ } }, "node_modules/@capacitor/keyboard": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.2.tgz", - "integrity": "sha512-he6xKmTBp5AhVrWJeEi6RYkJ25FjLLdNruBU2wafpITk3Nb7UdzOj96x3K6etFuEj8/rtn9WXBTs1o2XA86A1A==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.3.tgz", + "integrity": "sha512-27Bv5/2w1Ss2njguBgTS98O0Bb8DRJhAARyzXYib0JlT/n6BrJw/EZ0CokM4C8GFUjFDjJnEKF1Ie01buTMEXQ==", "dev": true, "license": "MIT", "peerDependencies": { From 0db5b4032778cc8dfb8378865a623b58d5692989 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:11:26 -0400 Subject: [PATCH 09/14] chore(deps): update actions/setup-node action to v6.4.0 (#31091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/setup-node](https://redirect.github.com/actions/setup-node) | action | minor | `v6.3.0` → `v6.4.0` | --- ### Release Notes
actions/setup-node (actions/setup-node) ### [`v6.4.0`](https://redirect.github.com/actions/setup-node/compare/v6.3.0...v6.4.0) [Compare Source](https://redirect.github.com/actions/setup-node/compare/v6.3.0...v6.4.0)
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - "every weekday before 11am" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Never, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/ionic-team/ionic-framework). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/actions/publish-npm/action.yml | 2 +- .github/workflows/actions/build-angular-server/action.yml | 2 +- .../workflows/actions/build-core-stencil-prerelease/action.yml | 2 +- .github/workflows/actions/build-core/action.yml | 2 +- .github/workflows/actions/build-react-router/action.yml | 2 +- .github/workflows/actions/build-react/action.yml | 2 +- .github/workflows/actions/build-vue-router/action.yml | 2 +- .github/workflows/actions/build-vue/action.yml | 2 +- .github/workflows/actions/test-angular-e2e/action.yml | 2 +- .github/workflows/actions/test-core-clean-build/action.yml | 2 +- .github/workflows/actions/test-core-lint/action.yml | 2 +- .github/workflows/actions/test-core-screenshot/action.yml | 2 +- .github/workflows/actions/test-core-spec/action.yml | 2 +- .github/workflows/actions/test-react-e2e/action.yml | 2 +- .github/workflows/actions/test-react-router-e2e/action.yml | 2 +- .github/workflows/actions/test-vue-e2e/action.yml | 2 +- .../workflows/actions/update-reference-screenshots/action.yml | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/actions/publish-npm/action.yml b/.github/actions/publish-npm/action.yml index 132b57f75b3..d3e06d5fb01 100644 --- a/.github/actions/publish-npm/action.yml +++ b/.github/actions/publish-npm/action.yml @@ -22,7 +22,7 @@ runs: using: 'composite' steps: - name: 🟢 Configure Node for Publish - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ inputs.node-version }} registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/actions/build-angular-server/action.yml b/.github/workflows/actions/build-angular-server/action.yml index 3cab52b650a..b5d37c5a9ac 100644 --- a/.github/workflows/actions/build-angular-server/action.yml +++ b/.github/workflows/actions/build-angular-server/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic Angular Server' runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-core-stencil-prerelease/action.yml b/.github/workflows/actions/build-core-stencil-prerelease/action.yml index 913e8f494ff..e23d9119831 100644 --- a/.github/workflows/actions/build-core-stencil-prerelease/action.yml +++ b/.github/workflows/actions/build-core-stencil-prerelease/action.yml @@ -9,7 +9,7 @@ runs: using: 'composite' steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x diff --git a/.github/workflows/actions/build-core/action.yml b/.github/workflows/actions/build-core/action.yml index 2b5117cf7af..7524c8a97b3 100644 --- a/.github/workflows/actions/build-core/action.yml +++ b/.github/workflows/actions/build-core/action.yml @@ -9,7 +9,7 @@ runs: using: 'composite' steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - name: 🕸️ Install Dependencies diff --git a/.github/workflows/actions/build-react-router/action.yml b/.github/workflows/actions/build-react-router/action.yml index 568c835c42f..c8083494b0a 100644 --- a/.github/workflows/actions/build-react-router/action.yml +++ b/.github/workflows/actions/build-react-router/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic React Router' runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-react/action.yml b/.github/workflows/actions/build-react/action.yml index 9b4a5995e9e..5899335ad3e 100644 --- a/.github/workflows/actions/build-react/action.yml +++ b/.github/workflows/actions/build-react/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic React' runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-vue-router/action.yml b/.github/workflows/actions/build-vue-router/action.yml index efd4579f565..9b07ce64973 100644 --- a/.github/workflows/actions/build-vue-router/action.yml +++ b/.github/workflows/actions/build-vue-router/action.yml @@ -3,7 +3,7 @@ description: 'Builds Ionic Vue Router' runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-vue/action.yml b/.github/workflows/actions/build-vue/action.yml index 170e889f968..5c7497ec359 100644 --- a/.github/workflows/actions/build-vue/action.yml +++ b/.github/workflows/actions/build-vue/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic Vue' runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-angular-e2e/action.yml b/.github/workflows/actions/test-angular-e2e/action.yml index 11aa8eb789c..a4835a0210a 100644 --- a/.github/workflows/actions/test-angular-e2e/action.yml +++ b/.github/workflows/actions/test-angular-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-core-clean-build/action.yml b/.github/workflows/actions/test-core-clean-build/action.yml index 92e3fed394b..96abc90121c 100644 --- a/.github/workflows/actions/test-core-clean-build/action.yml +++ b/.github/workflows/actions/test-core-clean-build/action.yml @@ -3,7 +3,7 @@ description: 'Test Core Clean Build' runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x diff --git a/.github/workflows/actions/test-core-lint/action.yml b/.github/workflows/actions/test-core-lint/action.yml index 321a2d26304..f9f0011719a 100644 --- a/.github/workflows/actions/test-core-lint/action.yml +++ b/.github/workflows/actions/test-core-lint/action.yml @@ -3,7 +3,7 @@ description: 'Test Core Lint' runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - name: 🕸️ Install Dependencies diff --git a/.github/workflows/actions/test-core-screenshot/action.yml b/.github/workflows/actions/test-core-screenshot/action.yml index 7ffa40faf5c..1f8699e66d4 100644 --- a/.github/workflows/actions/test-core-screenshot/action.yml +++ b/.github/workflows/actions/test-core-screenshot/action.yml @@ -13,7 +13,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-core-spec/action.yml b/.github/workflows/actions/test-core-spec/action.yml index f25207f6a49..2aab4b1be94 100644 --- a/.github/workflows/actions/test-core-spec/action.yml +++ b/.github/workflows/actions/test-core-spec/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - name: 🕸️ Install Dependencies diff --git a/.github/workflows/actions/test-react-e2e/action.yml b/.github/workflows/actions/test-react-e2e/action.yml index a6f1d42ba72..a1bcbf7a4db 100644 --- a/.github/workflows/actions/test-react-e2e/action.yml +++ b/.github/workflows/actions/test-react-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-react-router-e2e/action.yml b/.github/workflows/actions/test-react-router-e2e/action.yml index 70dff8db874..034cfdce747 100644 --- a/.github/workflows/actions/test-react-router-e2e/action.yml +++ b/.github/workflows/actions/test-react-router-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-vue-e2e/action.yml b/.github/workflows/actions/test-vue-e2e/action.yml index 060e923bdf4..191cd193c8a 100644 --- a/.github/workflows/actions/test-vue-e2e/action.yml +++ b/.github/workflows/actions/test-vue-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/update-reference-screenshots/action.yml b/.github/workflows/actions/update-reference-screenshots/action.yml index 51d7bdce508..6ee56689b10 100644 --- a/.github/workflows/actions/update-reference-screenshots/action.yml +++ b/.github/workflows/actions/update-reference-screenshots/action.yml @@ -7,7 +7,7 @@ on: runs: using: 'composite' steps: - - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 24.x - uses: actions/download-artifact@v8 From f3cd39b7fb291286374285c4a326ec6b9a8ea237 Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 27 Apr 2026 11:15:49 -0700 Subject: [PATCH 10/14] fix(modal): remove safe-area gap and flash in fullscreen modals (#31092) Issue number: resolves #31015 --------- ## What is the current behavior? Fullscreen modals without an `ion-footer` set `height` and `padding-bottom` on `.modal-wrapper` to reserve space for the bottom safe-area. Because `ion-content`'s background does not extend into that wrapper padding, a visible gap appears between the content and the bottom edge of the modal. Separately, modals that declare custom `--width` and `--height` on phone viewports flash inherited safe-area values while animating in, then snap to `0px` once the post-animation position correction runs. ## What is the new behavior? Bottom safe-area compensation now happens inside `ion-content` instead of on the wrapper, so the modal background stays edge-to-edge. - New internal `--ion-content-safe-area-padding-bottom` CSS property is added to `.inner-scroll`'s `padding-bottom` calc. - `modal.tsx` sets that property on `ion-content` for fullscreen modals without a footer, and clears it on resize and dismiss. - Wrapper no longer has `height` or `padding-bottom` inline styles written to it. - New `hasCustomModalDimensions()` helper detects modals that override both `--width` and `--height` to non-fullscreen values. These are zeroed from the initial safe-area prediction so there is no flash before the post-animation correction. - Extracted `findContentAndFooter()` and `applyFullscreenSafeAreaTo()` helpers so the resize handler and initial apply share the same lookup. - E2E tests cover scroll-padding placement, wrapper content area remaining full viewport height, `.inner-scroll` padding including safe-area, and custom-dimension modals starting at zero. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information `--ion-content-safe-area-padding-bottom` is an internal property. It is additive with the existing `--padding-bottom` consumers can set, and it only takes effect when `modal.tsx` writes it on fullscreen modals without a footer. Example pages: - iOS: https://ionic-framework-git-fw-7136-ionic1.vercel.app/src/components/modal/test/basic?ionic:mode=ios - MD: https://ionic-framework-git-fw-7136-ionic1.vercel.app/src/components/modal/test/basic?ionic:mode=md Example iOS card modal safe-area: - iOS: https://ionic-framework-git-fw-7136-ionic1.vercel.app/src/components/modal/test/safe-area?ionic:mode=ios ## Dev Build: ``` 8.8.5-dev.11776879142.101200b9 ``` --- core/src/components/content/content.scss | 2 +- core/src/components/modal/modal.tsx | 112 ++++++++++------ core/src/components/modal/safe-area-utils.ts | 36 +++++- .../modal/test/safe-area/modal.e2e.ts | 122 ++++++++++++++++-- 4 files changed, 218 insertions(+), 54 deletions(-) diff --git a/core/src/components/content/content.scss b/core/src/components/content/content.scss index 5f8b2afa831..89dae0aff94 100644 --- a/core/src/components/content/content.scss +++ b/core/src/components/content/content.scss @@ -64,7 +64,7 @@ .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 padding(calc(var(--padding-top) + var(--offset-top)), var(--padding-end), calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom) + var(--ion-content-safe-area-padding-bottom, 0px)), var(--padding-start)); position: absolute; diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index 9ba3ecbf009..5bbb67d3d61 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -50,6 +50,7 @@ import { applySafeAreaOverrides, clearSafeAreaOverrides, getRootSafeAreaTop, + hasCustomModalDimensions, type ModalSafeAreaContext, } from './safe-area-utils'; import { setCardStatusBarDark, setCardStatusBarDefault } from './utils'; @@ -311,12 +312,10 @@ export class Modal implements ComponentInterface, OverlayInterface { if (!context.isSheetModal && !context.isCardModal) { this.updateSafeAreaOverrides(); - // Re-evaluate fullscreen safe-area padding: clear first, then re-apply - if (this.wrapperEl) { - this.wrapperEl.style.removeProperty('height'); - this.wrapperEl.style.removeProperty('padding-bottom'); - } - this.applyFullscreenSafeArea(); + // Re-evaluate fullscreen safe-area padding: clear first, then re-apply. + const { contentEl, hasFooter } = this.findContentAndFooter(); + this.clearContentSafeAreaPadding(contentEl); + this.applyFullscreenSafeAreaTo(contentEl, hasFooter); } }, 50); // Debounce to avoid excessive calls during active resizing } @@ -1429,6 +1428,11 @@ export class Modal implements ComponentInterface, OverlayInterface { /** * Creates the context object for safe-area utilities. + * + * `hasCustomDimensions` is only set by `setInitialSafeAreaOverrides()` + * because it is only read by `getInitialSafeAreaConfig()`. Other callers + * (resize handler, post-animation update, fullscreen-padding apply) would + * pay a `getComputedStyle()` cost for a value they never consult. */ private getSafeAreaContext(): ModalSafeAreaContext { return { @@ -1451,7 +1455,10 @@ export class Modal implements ComponentInterface, OverlayInterface { * sheets to prevent header content from getting double-offset padding). */ private setInitialSafeAreaOverrides(): void { - const context = this.getSafeAreaContext(); + const context: ModalSafeAreaContext = { + ...this.getSafeAreaContext(), + hasCustomDimensions: hasCustomModalDimensions(this.el), + }; const safeAreaConfig = getInitialSafeAreaConfig(context); applySafeAreaOverrides(this.el, safeAreaConfig); @@ -1496,48 +1503,77 @@ export class Modal implements ComponentInterface, OverlayInterface { } /** - * Applies padding-bottom to fullscreen modal wrapper to prevent - * content from overlapping system navigation bar. + * Applies safe-area-bottom scroll padding to ion-content inside + * fullscreen modals that have no ion-footer. This prevents content + * from being hidden behind the system navigation bar while keeping + * the modal background edge-to-edge (no visible gap). */ private applyFullscreenSafeArea(): void { - const { wrapperEl, el } = this; - if (!wrapperEl) return; - const context = this.getSafeAreaContext(); if (context.isSheetModal || context.isCardModal) return; - // Check for standard Ionic layout children (ion-content, ion-footer), - // searching one level deep for wrapped components (e.g., - // ...). - // Note: uses a manual loop instead of querySelector(':scope > ...') because - // Stencil's mock-doc (used in spec tests) does not support :scope. - let hasContent = false; + const { contentEl, hasFooter } = this.findContentAndFooter(); + this.applyFullscreenSafeAreaTo(contentEl, hasFooter); + } + + /** + * Sets --ion-content-safe-area-padding-bottom on the given ion-content + * when no footer is present, so ion-content's .inner-scroll includes + * safe-area-bottom in its scroll padding. This keeps the modal background + * edge-to-edge while ensuring content scrolls clear of the system nav bar. + * + * --ion-content-safe-area-padding-bottom is an internal CSS property used + * only by this code path. It is not part of ion-content's public API and + * should not be set by consumers. The default of 0px makes it a no-op + * when unset, which is the expected state for ion-content used outside of + * a fullscreen modal without a footer. + */ + private applyFullscreenSafeAreaTo(contentEl: HTMLElement | null, hasFooter: boolean): void { + // Only apply for standard Ionic layouts (has ion-content but no + // ion-footer). When a footer is present it handles its own safe-area + // padding. Custom modals with raw HTML are developer-controlled. + if (!contentEl || hasFooter) return; + + contentEl.style.setProperty('--ion-content-safe-area-padding-bottom', 'var(--ion-safe-area-bottom, 0px)'); + } + + /** + * Removes the internal --ion-content-safe-area-padding-bottom property + * from an already-located ion-content. Callers do their own + * findContentAndFooter() so they can also read hasFooter if needed. + */ + private clearContentSafeAreaPadding(contentEl: HTMLElement | null): void { + if (!contentEl) return; + contentEl.style.removeProperty('--ion-content-safe-area-padding-bottom'); + } + + /** + * Finds ion-content and ion-footer among direct children and one level of + * grandchildren (for wrapped components like ). + * + * Intentionally does NOT use findIonContent() or querySelector() because + * those search the full subtree and would match ion-content inside nested + * routes/pages. We only want direct slot children (+ one wrapper level). + * + * Uses a manual loop instead of querySelector(':scope > ...') because + * Stencil's mock-doc (used in spec tests) does not support :scope. + */ + private findContentAndFooter(): { contentEl: HTMLElement | null; hasFooter: boolean } { + let contentEl: HTMLElement | null = null; let hasFooter = false; - for (const child of Array.from(el.children)) { - if (child.tagName === 'ION-CONTENT') hasContent = true; + for (const child of Array.from(this.el.children)) { + if (child.tagName === 'ION-CONTENT') contentEl = child as HTMLElement; if (child.tagName === 'ION-FOOTER') hasFooter = true; for (const grandchild of Array.from(child.children)) { - if (grandchild.tagName === 'ION-CONTENT') hasContent = true; + if (grandchild.tagName === 'ION-CONTENT' && !contentEl) contentEl = grandchild as HTMLElement; if (grandchild.tagName === 'ION-FOOTER') hasFooter = true; } } - - // Only apply wrapper padding for standard Ionic layouts (has ion-content - // but no ion-footer). Custom modals with raw HTML are fully - // developer-controlled and should not be modified. - if (!hasContent || hasFooter) return; - - // Reduce wrapper height by safe-area and add equivalent padding so the - // total visual size stays the same but the flex content area shrinks. - // Using height + padding instead of box-sizing: border-box avoids - // breaking custom modals that set --border-width (border-box would - // include the border inside the height, changing the layout). - wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))'); - wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)'); + return { contentEl, hasFooter }; } /** - * Clears all safe-area overrides and padding from wrapper. + * Clears all safe-area overrides and padding. */ private cleanupSafeAreaOverrides(): void { clearSafeAreaOverrides(this.el); @@ -1545,10 +1581,8 @@ export class Modal implements ComponentInterface, OverlayInterface { // Remove internal sheet offset property this.el.style.removeProperty('--ion-modal-offset-top'); - if (this.wrapperEl) { - this.wrapperEl.style.removeProperty('height'); - this.wrapperEl.style.removeProperty('padding-bottom'); - } + const { contentEl } = this.findContentAndFooter(); + this.clearContentSafeAreaPadding(contentEl); } render() { diff --git a/core/src/components/modal/safe-area-utils.ts b/core/src/components/modal/safe-area-utils.ts index a13bf1770a0..59ae3a4fdb5 100644 --- a/core/src/components/modal/safe-area-utils.ts +++ b/core/src/components/modal/safe-area-utils.ts @@ -23,6 +23,11 @@ export interface ModalSafeAreaContext { presentingElement?: HTMLElement; breakpoints?: number[]; currentBreakpoint?: number; + /** + * Only consulted by `getInitialSafeAreaConfig()`. Callers that only use the + * context for non-initial paths can omit this. See `hasCustomModalDimensions()`. + */ + hasCustomDimensions?: boolean; } /** @@ -38,6 +43,13 @@ const MODAL_INSET_MIN_WIDTH = 768; const MODAL_INSET_MIN_HEIGHT = 600; const EDGE_THRESHOLD = 5; +/** + * CSS values for `--width` / `--height` that are treated as fullscreen + * (modal touches the corresponding screen edges). Empty string means the + * property was not overridden. See `hasCustomModalDimensions()`. + */ +const FULLSCREEN_SIZE_VALUES = new Set(['', '100%', '100vw', '100vh', '100dvw', '100dvh', '100svw', '100svh']); + /** * Cache for resolved root safe-area-top value, invalidated once per frame. */ @@ -92,6 +104,23 @@ export const getRootSafeAreaTop = (): number => { return value; }; +/** + * True when the modal host declares BOTH a non-fullscreen `--width` AND a + * non-fullscreen `--height` (i.e. a centered-dialog-like modal that doesn't + * touch any screen edge). + * + * The conservative "both axes" check avoids mis-zeroing safe-area for + * partial-custom modals where the modal still touches top/bottom edges + * (e.g. only `--width` overridden). Partial cases fall through to the + * existing position-based post-animation correction. + */ +export const hasCustomModalDimensions = (hostEl: HTMLElement): boolean => { + const styles = getComputedStyle(hostEl); + const width = styles.getPropertyValue('--width').trim(); + const height = styles.getPropertyValue('--height').trim(); + return !FULLSCREEN_SIZE_VALUES.has(width) && !FULLSCREEN_SIZE_VALUES.has(height); +}; + /** * Returns the initial safe-area configuration based on modal type. * This is called before animation starts and uses configuration-based prediction. @@ -129,8 +158,11 @@ export const getInitialSafeAreaConfig = (context: ModalSafeAreaContext): SafeAre // On viewports that meet the centered dialog media query breakpoints, // regular modals render as centered dialogs (not fullscreen), so they - // don't touch any screen edges and don't need safe-area insets. - if (isCenteredDialogViewport()) { + // don't touch any screen edges and don't need safe-area insets. Also + // applies to phone viewports when the modal declares custom --width and + // --height; these don't touch screen edges either, so the initial + // prediction must be zero to avoid a post-animation correction flash. + if (isCenteredDialogViewport() || context.hasCustomDimensions) { return { top: '0px', bottom: '0px', diff --git a/core/src/components/modal/test/safe-area/modal.e2e.ts b/core/src/components/modal/test/safe-area/modal.e2e.ts index af2a58c7699..a65b6f3fee8 100644 --- a/core/src/components/modal/test/safe-area/modal.e2e.ts +++ b/core/src/components/modal/test/safe-area/modal.e2e.ts @@ -13,7 +13,11 @@ import { configs, test, Viewports } from '@utils/test/playwright'; * The test page (index.html) sets these root safe-area values. * Keep in sync with the :root block in test/safe-area/index.html. */ -const TEST_SAFE_AREA_TOP = '47px'; +const TEST_SAFE_AREA_TOP = 47; +const TEST_SAFE_AREA_BOTTOM = 34; +/** Default value of --ion-padding (16px), applied via the .ion-padding class on ion-content in the test modal. */ +const TEST_ION_PADDING = 16; + configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config }) => { test.describe(title('modal: safe-area handling'), () => { test.beforeEach(async ({ page }) => { @@ -100,10 +104,12 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config expect(safeAreaBottom).toBe('inherit'); }); - test('fullscreen modal without footer should have wrapper padding-bottom', async ({ page }, testInfo) => { + test('fullscreen modal without footer should set safe-area scroll padding on ion-content', async ({ + page, + }, testInfo) => { testInfo.annotations.push({ type: 'issue', - description: 'https://github.com/ionic-team/ionic-framework/issues/30900', + description: 'https://github.com/ionic-team/ionic-framework/issues/31015', }); const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); @@ -113,20 +119,83 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config const modal = page.locator('ion-modal'); - // When no footer is present, the wrapper should have reduced height - // and padding-bottom to prevent content from overlapping the system - // navigation bar, without changing box-sizing (which would break - // custom modals with --border-width). + // The wrapper should NOT have reduced height or padding-bottom. + // Safe-area compensation is handled by ion-content's scroll padding. const wrapper = modal.locator('.modal-wrapper'); - const paddingBottom = await wrapper.evaluate((el: HTMLElement) => { + const wrapperPaddingBottom = await wrapper.evaluate((el: HTMLElement) => { return el.style.getPropertyValue('padding-bottom'); }); - const height = await wrapper.evaluate((el: HTMLElement) => { + const wrapperHeight = await wrapper.evaluate((el: HTMLElement) => { return el.style.getPropertyValue('height'); }); - expect(paddingBottom).toBe('var(--ion-safe-area-bottom, 0px)'); - expect(height).toBe('calc(var(--height) - var(--ion-safe-area-bottom, 0px))'); + expect(wrapperPaddingBottom).toBe(''); + expect(wrapperHeight).toBe(''); + + // ion-content should have --ion-content-safe-area-padding-bottom set so its + // .inner-scroll element includes safe-area in its bottom padding. + const content = modal.locator('ion-content'); + const safeAreaPadding = await content.evaluate((el: HTMLElement) => { + return el.style.getPropertyValue('--ion-content-safe-area-padding-bottom'); + }); + expect(safeAreaPadding).toBe('var(--ion-safe-area-bottom, 0px)'); + }); + + test('fullscreen modal with ion-content and no footer should not reduce wrapper content area', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31015', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#fullscreen-modal-no-footer'); + await ionModalDidPresent.next(); + + const modal = page.locator('ion-modal'); + const wrapper = modal.locator('.modal-wrapper'); + + // The wrapper's content area should equal the full viewport height. + // Safe-area compensation is handled by ion-content's scroll padding, + // not by reducing the wrapper. This prevents the visible white gap + // reported in #31015. + const { contentHeight, paddingBottom } = await wrapper.evaluate((el: HTMLElement) => { + const computed = getComputedStyle(el); + return { + contentHeight: parseFloat(computed.height), + paddingBottom: parseFloat(computed.paddingBottom), + }; + }); + const viewportHeight = await page.evaluate(() => window.innerHeight); + + expect(paddingBottom).toBeCloseTo(0, 0); + expect(contentHeight).toBeCloseTo(viewportHeight, 0); + }); + + test('fullscreen modal ion-content scroll padding should include safe-area-bottom', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31015', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#fullscreen-modal-no-footer'); + await ionModalDidPresent.next(); + + const modal = page.locator('ion-modal'); + const content = modal.locator('ion-content'); + + // The .inner-scroll element inside ion-content's shadow DOM should + // have padding-bottom that includes the safe-area-bottom value. + const innerScroll = content.locator('.inner-scroll'); + const scrollPaddingBottom = await innerScroll.evaluate((el: Element) => { + return parseFloat(getComputedStyle(el).paddingBottom); + }); + + expect(scrollPaddingBottom).toBe(TEST_ION_PADDING + TEST_SAFE_AREA_BOTTOM); }); test('sheet modal at breakpoint 1 should keep top safe-area zeroed', async ({ page }, testInfo) => { @@ -185,7 +254,7 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config const offsetTop = await modal.evaluate((el: HTMLIonModalElement) => { return el.style.getPropertyValue('--ion-modal-offset-top'); }); - expect(offsetTop).toBe(TEST_SAFE_AREA_TOP); + expect(offsetTop).toBe(`${TEST_SAFE_AREA_TOP}px`); }); test('fullscreen modal safe-area should update on resize from phone to tablet', async ({ page }, testInfo) => { @@ -251,6 +320,35 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config expect(safeAreaBottom).toBe('0px'); }); + test('centered dialog with custom dimensions on phone should zero safe-area from initial prediction', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/31015', + }); + + // Stay on phone viewport. This is the path where the centered-dialog + // media query does NOT match but the modal still doesn't touch screen + // edges because cssClass sets --width/--height. Without the initial + // prediction catching this, safe-area flashes inherited values and + // then snaps to 0px after animation. + const ionModalWillPresent = await page.spyOnEvent('ionModalWillPresent'); + await page.click('#centered-dialog'); + await ionModalWillPresent.next(); + + // Read inline style IMMEDIATELY after will-present fires, before the + // animation finishes. This captures the initial prediction value. + const modal = page.locator('ion-modal'); + const initial = await modal.evaluate((el: HTMLIonModalElement) => ({ + top: el.style.getPropertyValue('--ion-safe-area-top'), + bottom: el.style.getPropertyValue('--ion-safe-area-bottom'), + })); + + expect(initial.top).toBe('0px'); + expect(initial.bottom).toBe('0px'); + }); + test('safe-area overrides should be cleared on dismiss', async ({ page }, testInfo) => { testInfo.annotations.push({ type: 'issue', From fd79771e5be77c9f38379a3a7b9ab44bb11ff325 Mon Sep 17 00:00:00 2001 From: Shane Date: Wed, 29 Apr 2026 06:51:21 -0700 Subject: [PATCH 11/14] fix(select): select focused option on Enter in popover and modal interfaces (#31093) Issue number: resolves #30561 --------- ## What is the current behavior? When an `ion-select` with `interface="popover"` or `interface="modal"` is opened with the keyboard, pressing Enter on a focused option doesn't commit the value or dismiss the overlay. Only Space works. The `onKeyUp` handlers in `select-popover.tsx` and `select-modal.tsx` only checked for `' '`, despite the comment claiming Enter was supported, and `ion-radio-group`'s arrow-key handler never committed the focused value on Enter the way native `` behavior. `radio-group.tsx` gains an Enter branch inside its select-interface arrow-key handler that sets `value` to the focused radio and emits `ionChange` when it differs from the previous value. `select-popover.tsx` and `select-modal.tsx` track the `onKeyDown` target so `onKeyUp` only dismisses when the Enter press started on the same option. That guard stops a held Enter on the triggering `ion-select` from re-firing inside the just-opened overlay and auto-dismissing it. The `!ev.repeat` check on keydown covers the same case for the radio-group commit. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information The keydown/keyup target-matching pattern is the same trick the browser uses to avoid firing a click when keydown and keyup happen on different elements. It's needed here because Enter on the `ion-select` trigger opens the overlay on keydown, and without the guard the corresponding keyup (now inside the overlay) would immediately commit and dismiss. Preview page: - [select / basic](https://ionic-framework-git-fw-6754-ionic1.vercel.app/src/components/select/test/basic?ionic) --- .../components/radio-group/radio-group.tsx | 13 + .../radio-group/test/basic/radio-group.e2e.ts | 26 ++ .../components/radio-group/test/fixtures.ts | 4 +- .../components/select-modal/select-modal.tsx | 22 +- .../test/basic/select-modal.e2e.ts | 29 ++ .../components/select-modal/test/fixtures.ts | 5 + .../select-popover/select-popover.tsx | 23 +- .../test/basic/select-popover.e2e.ts | 29 ++ .../select-popover/test/fixtures.ts | 5 + .../select/test/basic/select.e2e.ts | 441 ++++++++++++++++++ 10 files changed, 585 insertions(+), 12 deletions(-) diff --git a/core/src/components/radio-group/radio-group.tsx b/core/src/components/radio-group/radio-group.tsx index 88ff48e2c4c..37e0f0da0c9 100644 --- a/core/src/components/radio-group/radio-group.tsx +++ b/core/src/components/radio-group/radio-group.tsx @@ -290,6 +290,19 @@ export class RadioGroup implements ComponentInterface { // to the bottom of the screen ev.preventDefault(); } + + // Inside a select interface, Enter commits the focused radio + // value (matching native