diff --git a/core/src/components/footer/footer.scss b/core/src/components/footer/footer.scss index 9682747b41..c6b9c24d15 100644 --- a/core/src/components/footer/footer.scss +++ b/core/src/components/footer/footer.scss @@ -14,6 +14,6 @@ ion-footer { 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); } \ No newline at end of file diff --git a/core/src/components/footer/footer.tsx b/core/src/components/footer/footer.tsx index 7e0ae4b9ab..a52eccccff 100644 --- a/core/src/components/footer/footer.tsx +++ b/core/src/components/footer/footer.tsx @@ -1,8 +1,10 @@ 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 { 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'; @@ -19,6 +21,9 @@ import { handleFooterFade } from './footer.utils'; export class Footer implements ComponentInterface { private scrollEl?: HTMLElement; private contentScrollCallback: any; + private keyboardCtrl: KeyboardController | null = null; + + @State() private keyboardVisible = false; @Element() el!: HTMLIonFooterElement; @@ -46,6 +51,18 @@ export class Footer implements ComponentInterface { 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 = () => { const mode = getIonMode(this); if (mode !== 'ios') { @@ -94,6 +111,9 @@ export class Footer implements ComponentInterface { render() { const { translucent, collapse } = this; const mode = getIonMode(this); + const tabs = this.el.closest('ion-tabs'); + const tabBar = tabs?.querySelector('ion-tab-bar'); + return ( { + 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`); + }); +}); diff --git a/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..e6e81adaa0 Binary files /dev/null and b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..15dc2a4492 Binary files /dev/null and b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Safari-linux.png b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..a9ea0358ce Binary files /dev/null and b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Chrome-linux.png b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Chrome-linux.png new file mode 100644 index 0000000000..bdab430c25 Binary files /dev/null and b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Firefox-linux.png b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Firefox-linux.png new file mode 100644 index 0000000000..0cb7bebaab Binary files /dev/null and b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Safari-linux.png b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Safari-linux.png new file mode 100644 index 0000000000..6d0b17bdf3 Binary files /dev/null and b/core/src/components/footer/test/with-tabs/footer.e2e.ts-snapshots/footer-with-tabs-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/footer/test/with-tabs/index.html b/core/src/components/footer/test/with-tabs/index.html new file mode 100644 index 0000000000..a1bc4b9ff5 --- /dev/null +++ b/core/src/components/footer/test/with-tabs/index.html @@ -0,0 +1,71 @@ + + + + + Footer - With Tabs + + + + + + + + + + + + + + + + Tab One + + + +

Tab One

+
+ + + Footer + + +
+ + + + + Tab Two + + + +

Tab Two

+
+ + + Footer + + +
+ + + + Tab One + + + + + Tab Two + + + +
+
+ + diff --git a/core/src/components/tab-bar/tab-bar.tsx b/core/src/components/tab-bar/tab-bar.tsx index 2eb7251927..2ca61f5eb5 100644 --- a/core/src/components/tab-bar/tab-bar.tsx +++ b/core/src/components/tab-bar/tab-bar.tsx @@ -1,5 +1,7 @@ import type { ComponentInterface, EventEmitter } 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 type { Color, TabBarChangedEventDetail } from '../../interface'; @@ -17,8 +19,7 @@ import { createColorClasses } from '../../utils/theme'; shadow: true, }) export class TabBar implements ComponentInterface { - private keyboardWillShowHandler?: () => void; - private keyboardWillHideHandler?: () => void; + private keyboardCtrl: KeyboardController | null = null; @Element() el!: HTMLElement; @@ -59,43 +60,30 @@ export class TabBar implements ComponentInterface { } connectedCallback() { - if (typeof (window as any) !== 'undefined') { - this.keyboardWillShowHandler = () => { - 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!); - } + this.keyboardCtrl = createKeyboardController((keyboardOpen) => { + this.keyboardVisible = keyboardOpen; // trigger re-render by updating state + }); } disconnectedCallback() { - if (typeof (window as any) !== 'undefined') { - window.removeEventListener('keyboardWillShow', this.keyboardWillShowHandler!); - window.removeEventListener('keyboardWillHide', this.keyboardWillHideHandler!); - - this.keyboardWillShowHandler = this.keyboardWillHideHandler = undefined; + if (this.keyboardCtrl) { + this.keyboardCtrl.destroy(); } } render() { const { color, translucent, keyboardVisible } = this; const mode = getIonMode(this); + const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top'; return ( diff --git a/core/src/utils/keyboard/keyboard-controller.ts b/core/src/utils/keyboard/keyboard-controller.ts new file mode 100644 index 0000000000..845bb6f1d0 --- /dev/null +++ b/core/src/utils/keyboard/keyboard-controller.ts @@ -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; +}; diff --git a/core/src/utils/keyboard/test/keyboard-controller.spec.ts b/core/src/utils/keyboard/test/keyboard-controller.spec.ts new file mode 100644 index 0000000000..e623ac9d10 --- /dev/null +++ b/core/src/utils/keyboard/test/keyboard-controller.spec.ts @@ -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); + }); +});