mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
fix(footer): remove toolbar bottom padding if near bottom slot tabs or keyboard is open (#25746)
Co-authored-by: EinfachHans <EinfachHans@users.noreply.github.com>
This commit is contained in:
@ -14,6 +14,6 @@ ion-footer {
|
|||||||
z-index: $z-index-toolbar;
|
z-index: $z-index-toolbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-footer ion-toolbar:last-of-type {
|
ion-footer.footer-toolbar-padding ion-toolbar:last-of-type {
|
||||||
padding-bottom: var(--ion-safe-area-bottom, 0);
|
padding-bottom: var(--ion-safe-area-bottom, 0);
|
||||||
}
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import type { ComponentInterface } from '@stencil/core';
|
import type { ComponentInterface } from '@stencil/core';
|
||||||
import { Component, Element, Host, Prop, h } from '@stencil/core';
|
import { Component, Element, Host, Prop, State, h } from '@stencil/core';
|
||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import { findIonContent, getScrollElement, printIonContentErrorMsg } from '../../utils/content';
|
import { findIonContent, getScrollElement, printIonContentErrorMsg } from '../../utils/content';
|
||||||
|
import type { KeyboardController } from '../../utils/keyboard/keyboard-controller';
|
||||||
|
import { createKeyboardController } from '../../utils/keyboard/keyboard-controller';
|
||||||
|
|
||||||
import { handleFooterFade } from './footer.utils';
|
import { handleFooterFade } from './footer.utils';
|
||||||
|
|
||||||
@ -19,6 +21,9 @@ import { handleFooterFade } from './footer.utils';
|
|||||||
export class Footer implements ComponentInterface {
|
export class Footer implements ComponentInterface {
|
||||||
private scrollEl?: HTMLElement;
|
private scrollEl?: HTMLElement;
|
||||||
private contentScrollCallback: any;
|
private contentScrollCallback: any;
|
||||||
|
private keyboardCtrl: KeyboardController | null = null;
|
||||||
|
|
||||||
|
@State() private keyboardVisible = false;
|
||||||
|
|
||||||
@Element() el!: HTMLIonFooterElement;
|
@Element() el!: HTMLIonFooterElement;
|
||||||
|
|
||||||
@ -46,6 +51,18 @@ export class Footer implements ComponentInterface {
|
|||||||
this.checkCollapsibleFooter();
|
this.checkCollapsibleFooter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.keyboardCtrl = createKeyboardController((keyboardOpen) => {
|
||||||
|
this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (this.keyboardCtrl) {
|
||||||
|
this.keyboardCtrl.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private checkCollapsibleFooter = () => {
|
private checkCollapsibleFooter = () => {
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
if (mode !== 'ios') {
|
if (mode !== 'ios') {
|
||||||
@ -94,6 +111,9 @@ export class Footer implements ComponentInterface {
|
|||||||
render() {
|
render() {
|
||||||
const { translucent, collapse } = this;
|
const { translucent, collapse } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
|
const tabs = this.el.closest('ion-tabs');
|
||||||
|
const tabBar = tabs?.querySelector('ion-tab-bar');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
role="contentinfo"
|
role="contentinfo"
|
||||||
@ -105,6 +125,7 @@ export class Footer implements ComponentInterface {
|
|||||||
|
|
||||||
[`footer-translucent`]: translucent,
|
[`footer-translucent`]: translucent,
|
||||||
[`footer-translucent-${mode}`]: translucent,
|
[`footer-translucent-${mode}`]: translucent,
|
||||||
|
['footer-toolbar-padding']: !this.keyboardVisible && (!tabBar || tabBar.slot !== 'bottom'),
|
||||||
|
|
||||||
[`footer-collapse-${collapse}`]: collapse !== undefined,
|
[`footer-collapse-${collapse}`]: collapse !== undefined,
|
||||||
}}
|
}}
|
||||||
|
13
core/src/components/footer/test/with-tabs/footer.e2e.ts
Normal file
13
core/src/components/footer/test/with-tabs/footer.e2e.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import { test } from '@utils/test/playwright';
|
||||||
|
|
||||||
|
test.describe('footer: with tabs', () => {
|
||||||
|
test('should not have extra padding when near a tab bar', async ({ page }, testInfo) => {
|
||||||
|
test.skip(testInfo.project.metadata.rtl === true, 'This does not test LTR vs. RTL layout.');
|
||||||
|
|
||||||
|
await page.goto('/src/components/footer/test/with-tabs');
|
||||||
|
|
||||||
|
const footer = page.locator('[tab="tab-one"] ion-footer');
|
||||||
|
expect(await footer.screenshot()).toMatchSnapshot(`footer-with-tabs-${page.getSnapshotSettings()}.png`);
|
||||||
|
});
|
||||||
|
});
|
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
71
core/src/components/footer/test/with-tabs/index.html
Normal file
71
core/src/components/footer/test/with-tabs/index.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Footer - With Tabs</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--ion-safe-area-bottom: 40px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-tabs>
|
||||||
|
<ion-tab tab="tab-one">
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Tab One</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<h1>Tab One</h1>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Footer</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ion-tab>
|
||||||
|
|
||||||
|
<ion-tab tab="tab-two">
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Tab Two</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content class="ion-padding">
|
||||||
|
<h1>Tab Two</h1>
|
||||||
|
</ion-content>
|
||||||
|
<ion-footer>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Footer</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-footer>
|
||||||
|
</ion-tab>
|
||||||
|
|
||||||
|
<ion-tab-bar slot="bottom">
|
||||||
|
<ion-tab-button tab="tab-one">
|
||||||
|
<ion-label>Tab One</ion-label>
|
||||||
|
<ion-icon name="star"></ion-icon>
|
||||||
|
</ion-tab-button>
|
||||||
|
|
||||||
|
<ion-tab-button tab="tab-two">
|
||||||
|
<ion-label>Tab Two</ion-label>
|
||||||
|
<ion-icon name="globe"></ion-icon>
|
||||||
|
</ion-tab-button>
|
||||||
|
</ion-tab-bar>
|
||||||
|
</ion-tabs>
|
||||||
|
</ion-app>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,5 +1,7 @@
|
|||||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||||
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
|
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
|
||||||
|
import type { KeyboardController } from '@utils/keyboard/keyboard-controller';
|
||||||
|
import { createKeyboardController } from '@utils/keyboard/keyboard-controller';
|
||||||
|
|
||||||
import { getIonMode } from '../../global/ionic-global';
|
import { getIonMode } from '../../global/ionic-global';
|
||||||
import type { Color, TabBarChangedEventDetail } from '../../interface';
|
import type { Color, TabBarChangedEventDetail } from '../../interface';
|
||||||
@ -17,8 +19,7 @@ import { createColorClasses } from '../../utils/theme';
|
|||||||
shadow: true,
|
shadow: true,
|
||||||
})
|
})
|
||||||
export class TabBar implements ComponentInterface {
|
export class TabBar implements ComponentInterface {
|
||||||
private keyboardWillShowHandler?: () => void;
|
private keyboardCtrl: KeyboardController | null = null;
|
||||||
private keyboardWillHideHandler?: () => void;
|
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLElement;
|
||||||
|
|
||||||
@ -59,43 +60,30 @@ export class TabBar implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
if (typeof (window as any) !== 'undefined') {
|
this.keyboardCtrl = createKeyboardController((keyboardOpen) => {
|
||||||
this.keyboardWillShowHandler = () => {
|
this.keyboardVisible = keyboardOpen; // trigger re-render by updating state
|
||||||
if (this.el.getAttribute('slot') !== 'top') {
|
});
|
||||||
this.keyboardVisible = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.keyboardWillHideHandler = () => {
|
|
||||||
setTimeout(() => (this.keyboardVisible = false), 50);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keyboardWillShow', this.keyboardWillShowHandler!);
|
|
||||||
window.addEventListener('keyboardWillHide', this.keyboardWillHideHandler!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if (typeof (window as any) !== 'undefined') {
|
if (this.keyboardCtrl) {
|
||||||
window.removeEventListener('keyboardWillShow', this.keyboardWillShowHandler!);
|
this.keyboardCtrl.destroy();
|
||||||
window.removeEventListener('keyboardWillHide', this.keyboardWillHideHandler!);
|
|
||||||
|
|
||||||
this.keyboardWillShowHandler = this.keyboardWillHideHandler = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { color, translucent, keyboardVisible } = this;
|
const { color, translucent, keyboardVisible } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
|
const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
role="tablist"
|
role="tablist"
|
||||||
aria-hidden={keyboardVisible ? 'true' : null}
|
aria-hidden={shouldHide ? 'true' : null}
|
||||||
class={createColorClasses(color, {
|
class={createColorClasses(color, {
|
||||||
[mode]: true,
|
[mode]: true,
|
||||||
'tab-bar-translucent': translucent,
|
'tab-bar-translucent': translucent,
|
||||||
'tab-bar-hidden': keyboardVisible,
|
'tab-bar-hidden': shouldHide,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
48
core/src/utils/keyboard/keyboard-controller.ts
Normal file
48
core/src/utils/keyboard/keyboard-controller.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { win } from '../window';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a controller that tracks and reacts to opening or closing the keyboard.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @param keyboardChangeCallback A function to call when the keyboard opens or closes.
|
||||||
|
*/
|
||||||
|
export const createKeyboardController = (
|
||||||
|
keyboardChangeCallback?: (keyboardOpen: boolean) => void
|
||||||
|
): KeyboardController => {
|
||||||
|
let keyboardWillShowHandler: (() => void) | undefined;
|
||||||
|
let keyboardWillHideHandler: (() => void) | undefined;
|
||||||
|
let keyboardVisible: boolean;
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
keyboardWillShowHandler = () => {
|
||||||
|
keyboardVisible = true;
|
||||||
|
if (keyboardChangeCallback) keyboardChangeCallback(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
keyboardWillHideHandler = () => {
|
||||||
|
keyboardVisible = false;
|
||||||
|
if (keyboardChangeCallback) keyboardChangeCallback(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
win?.addEventListener('keyboardWillShow', keyboardWillShowHandler);
|
||||||
|
win?.addEventListener('keyboardWillHide', keyboardWillHideHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
win?.removeEventListener('keyboardWillShow', keyboardWillShowHandler!);
|
||||||
|
win?.removeEventListener('keyboardWillHide', keyboardWillHideHandler!);
|
||||||
|
|
||||||
|
keyboardWillShowHandler = keyboardWillHideHandler = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isKeyboardVisible = () => keyboardVisible;
|
||||||
|
|
||||||
|
init();
|
||||||
|
return { init, destroy, isKeyboardVisible };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KeyboardController = {
|
||||||
|
init: () => void;
|
||||||
|
destroy: () => void;
|
||||||
|
isKeyboardVisible: () => boolean;
|
||||||
|
};
|
24
core/src/utils/keyboard/test/keyboard-controller.spec.ts
Normal file
24
core/src/utils/keyboard/test/keyboard-controller.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createKeyboardController } from '../keyboard-controller';
|
||||||
|
|
||||||
|
describe('Keyboard Controller', () => {
|
||||||
|
it('should update isKeyboardVisible', () => {
|
||||||
|
const keyboardCtrl = createKeyboardController();
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('keyboardWillShow'));
|
||||||
|
expect(keyboardCtrl.isKeyboardVisible()).toBe(true);
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('keyboardWillHide'));
|
||||||
|
expect(keyboardCtrl.isKeyboardVisible()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run the callback', () => {
|
||||||
|
const callbackMock = jest.fn();
|
||||||
|
createKeyboardController(callbackMock);
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('keyboardWillShow'));
|
||||||
|
expect(callbackMock).toHaveBeenCalledWith(true);
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('keyboardWillHide'));
|
||||||
|
expect(callbackMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user