Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/components/footer/footer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
23 changes: 22 additions & 1 deletion core/src/components/footer/footer.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;

Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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');
Comment thread
averyrousseau marked this conversation as resolved.

return (
<Host
role="contentinfo"
Expand All @@ -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,
}}
Expand Down
13 changes: 13 additions & 0 deletions core/src/components/footer/test/with-tabs/footer.e2e.ts
Original file line number Diff line number Diff line change
@@ -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');
Comment thread
averyrousseau marked this conversation as resolved.

const footer = page.locator('[tab="tab-one"] ion-footer');
expect(await footer.screenshot()).toMatchSnapshot(`footer-with-tabs-${page.getSnapshotSettings()}.png`);
Comment thread
liamdebeasi marked this conversation as resolved.
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions core/src/components/footer/test/with-tabs/index.html
Original file line number Diff line number Diff line change
@@ -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>
34 changes: 11 additions & 23 deletions core/src/components/tab-bar/tab-bar.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;

Expand Down Expand Up @@ -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>
Expand Down
48 changes: 48 additions & 0 deletions core/src/utils/keyboard/keyboard-controller.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions core/src/utils/keyboard/test/keyboard-controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});