Skip to content

Commit 095b72e

Browse files
committed
fix(content): detect dynamic tab bar changes for safe-area handling
1 parent e953f7b commit 095b72e

File tree

3 files changed

+102
-1
lines changed

3 files changed

+102
-1
lines changed

core/src/components/content/content.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ export class Content implements ComponentInterface {
4747
/** Watches for dynamic header/footer changes in parent element */
4848
private parentMutationObserver?: MutationObserver;
4949

50+
/** Watches for dynamic tab bar changes in ion-tabs */
51+
private tabsMutationObserver?: MutationObserver;
52+
5053
private tabsElement: HTMLElement | null = null;
5154
private tabsLoadCallback?: () => void;
5255

@@ -213,6 +216,20 @@ export class Content implements ComponentInterface {
213216
});
214217
this.parentMutationObserver.observe(parent, { childList: true });
215218
}
219+
220+
// Watch for dynamic tab bar changes in ion-tabs (common in Angular conditional rendering)
221+
const tabs = this.el.closest('ion-tabs');
222+
if (tabs && !this.tabsMutationObserver && win !== undefined && 'MutationObserver' in win) {
223+
this.tabsMutationObserver = new MutationObserver(() => {
224+
const prevHasFooter = this.hasFooter;
225+
this.updateSiblingDetection();
226+
// Only trigger re-render if footer detection actually changed
227+
if (prevHasFooter !== this.hasFooter) {
228+
forceUpdate(this);
229+
}
230+
});
231+
this.tabsMutationObserver.observe(tabs, { childList: true });
232+
}
216233
}
217234

218235
/**
@@ -264,9 +281,11 @@ export class Content implements ComponentInterface {
264281
disconnectedCallback() {
265282
this.onScrollEnd();
266283

267-
// Clean up mutation observer to prevent memory leaks
284+
// Clean up mutation observers to prevent memory leaks
268285
this.parentMutationObserver?.disconnect();
269286
this.parentMutationObserver = undefined;
287+
this.tabsMutationObserver?.disconnect();
288+
this.tabsMutationObserver = undefined;
270289

271290
if (hasLazyBuild(this.el)) {
272291
/**

core/src/components/content/test/safe-area/content.e2e.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,5 +164,53 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
164164
// Should have safe-area-top again
165165
await expect(content).toHaveClass(/safe-area-top/, { timeout: 1000 });
166166
});
167+
168+
test('content inside ion-tabs with tab bar should not have safe-area-bottom', async ({ page }, testInfo) => {
169+
testInfo.annotations.push({
170+
type: 'issue',
171+
description: 'https://github.com/ionic-team/ionic-framework/issues/30900',
172+
});
173+
174+
const content = page.locator('#content-dynamic-tabs');
175+
// Tab bar is present, so content should not have safe-area-bottom
176+
await expect(content).not.toHaveClass(/safe-area-bottom/);
177+
});
178+
179+
test('dynamic tab bar removal should update safe-area classes', async ({ page }, testInfo) => {
180+
testInfo.annotations.push({
181+
type: 'issue',
182+
description: 'https://github.com/ionic-team/ionic-framework/issues/30900',
183+
});
184+
185+
const content = page.locator('#content-dynamic-tabs');
186+
187+
// Initially tab bar is present, so no safe-area-bottom
188+
await expect(content).not.toHaveClass(/safe-area-bottom/);
189+
190+
// Remove tab bar
191+
await page.evaluate(() => (window as any).removeTabBar());
192+
193+
// Should have safe-area-bottom now
194+
await expect(content).toHaveClass(/safe-area-bottom/, { timeout: 1000 });
195+
});
196+
197+
test('dynamic tab bar addition should update safe-area classes', async ({ page }, testInfo) => {
198+
testInfo.annotations.push({
199+
type: 'issue',
200+
description: 'https://github.com/ionic-team/ionic-framework/issues/30900',
201+
});
202+
203+
const content = page.locator('#content-dynamic-tabs');
204+
205+
// Remove tab bar first
206+
await page.evaluate(() => (window as any).removeTabBar());
207+
await expect(content).toHaveClass(/safe-area-bottom/, { timeout: 1000 });
208+
209+
// Add tab bar back
210+
await page.evaluate(() => (window as any).addTabBar());
211+
212+
// Should not have safe-area-bottom anymore
213+
await expect(content).not.toHaveClass(/safe-area-bottom/, { timeout: 1000 });
214+
});
167215
});
168216
});

core/src/components/content/test/safe-area/index.html

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,22 @@
163163
</div>
164164
</div>
165165

166+
<!-- Test 10: Dynamic tab bar - for testing ion-tabs mutation observer -->
167+
<div id="test-dynamic-tabs" class="test-section">
168+
<ion-tabs id="dynamic-tabs">
169+
<div class="ion-page" id="dynamic-tabs-page">
170+
<ion-content id="content-dynamic-tabs">
171+
<p>Content with dynamic tab bar</p>
172+
</ion-content>
173+
</div>
174+
<ion-tab-bar id="dynamic-tab-bar" slot="bottom">
175+
<ion-tab-button tab="tab1">
176+
<ion-label>Tab 1</ion-label>
177+
</ion-tab-button>
178+
</ion-tab-bar>
179+
</ion-tabs>
180+
</div>
181+
166182
<script>
167183
function addHeader() {
168184
const page = document.getElementById('dynamic-page');
@@ -186,6 +202,24 @@
186202
function openModal() {
187203
document.getElementById('test-modal').isOpen = true;
188204
}
205+
206+
function addTabBar() {
207+
const tabs = document.getElementById('dynamic-tabs');
208+
if (!tabs.querySelector('ion-tab-bar')) {
209+
const tabBar = document.createElement('ion-tab-bar');
210+
tabBar.id = 'dynamic-tab-bar';
211+
tabBar.slot = 'bottom';
212+
tabBar.innerHTML = '<ion-tab-button tab="tab1"><ion-label>Tab 1</ion-label></ion-tab-button>';
213+
tabs.appendChild(tabBar);
214+
}
215+
}
216+
217+
function removeTabBar() {
218+
const tabBar = document.getElementById('dynamic-tab-bar');
219+
if (tabBar) {
220+
tabBar.remove();
221+
}
222+
}
189223
</script>
190224
</ion-app>
191225
</body>

0 commit comments

Comments
 (0)