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:
Amanda Johnston
2022-08-16 13:22:43 -05:00
committed by GitHub
parent 79c65dc382
commit bb37446032
13 changed files with 190 additions and 25 deletions

View File

@ -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);
}

View File

@ -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 (
<Host
role="contentinfo"
@ -105,6 +125,7 @@ export class Footer implements ComponentInterface {
[`footer-translucent`]: translucent,
[`footer-translucent-${mode}`]: translucent,
['footer-toolbar-padding']: !this.keyboardVisible && (!tabBar || tabBar.slot !== 'bottom'),
[`footer-collapse-${collapse}`]: collapse !== undefined,
}}

View 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: 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

View 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>

View File

@ -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 (
<Host
role="tablist"
aria-hidden={keyboardVisible ? 'true' : null}
aria-hidden={shouldHide ? 'true' : null}
class={createColorClasses(color, {
[mode]: true,
'tab-bar-translucent': translucent,
'tab-bar-hidden': keyboardVisible,
'tab-bar-hidden': shouldHide,
})}
>
<slot></slot>

View 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;
};

View 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);
});
});