Skip to content

Commit 5802e6f

Browse files
committed
fix(overlay): account for virtual keyboard offset on mobile devices
This is taking over from #11947. Fixes the connected overlay positioning being thrown off, if it is opened while an input is focused (e.g. when building an autocomplete component). The issue comes from the fact that the browser will offset the entire page in order to put the input in the middle and to make space for the virtual keyboard. Fixes #6341.
1 parent a410865 commit 5802e6f

File tree

3 files changed

+55
-5
lines changed

3 files changed

+55
-5
lines changed

src/cdk/overlay/position/flexible-connected-position-strategy.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,39 @@ describe('FlexibleConnectedPositionStrategy', () => {
118118
document.body.removeChild(origin);
119119
});
120120

121+
it('should for the virtual keyboard offset when positioning the overlay', () => {
122+
const originElement = createPositionedBlockElement();
123+
document.body.appendChild(originElement);
124+
125+
// Position the element so it would have enough space to fit.
126+
originElement.style.top = '200px';
127+
originElement.style.left = '70px';
128+
129+
// Pull the element up ourselves to simulate what a mobile
130+
// browser would do when the virtual keyboard is being shown.
131+
overlayContainer.getContainerElement().style.top = '-100px';
132+
133+
attachOverlay({
134+
positionStrategy: overlay.position()
135+
.flexibleConnectedTo(originElement)
136+
.withFlexibleDimensions(false)
137+
.withPush(false)
138+
.withPositions([{
139+
originX: 'start',
140+
originY: 'bottom',
141+
overlayX: 'start',
142+
overlayY: 'top'
143+
}])
144+
});
145+
146+
const originRect = originElement.getBoundingClientRect();
147+
const overlayRect = overlayRef.overlayElement.getBoundingClientRect();
148+
149+
expect(Math.floor(overlayRect.top)).toBe(Math.floor(originRect.bottom));
150+
151+
document.body.removeChild(originElement);
152+
});
153+
121154
describe('without flexible dimensions and pushing', () => {
122155
const ORIGIN_HEIGHT = DEFAULT_HEIGHT;
123156
const ORIGIN_WIDTH = DEFAULT_WIDTH;

src/cdk/overlay/position/flexible-connected-position-strategy.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {OverlayReference} from '../overlay-reference';
2121
import {isElementScrolledOutsideView, isElementClippedByScrolling} from './scroll-clip';
2222
import {coerceCssPixelValue} from '@angular/cdk/coercion';
2323
import {Platform} from '@angular/cdk/platform';
24+
import {OverlayContainer} from '../overlay-container';
2425

2526
// TODO: refactor clipping detection into a separate thing (part of scrolling module)
2627
// TODO: doesn't handle both flexible width and height when it has to scroll along both axis.
@@ -131,8 +132,9 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
131132
connectedTo: ElementRef | HTMLElement,
132133
private _viewportRuler: ViewportRuler,
133134
private _document: Document,
134-
// @deletion-target 7.0.0 `_platform` parameter to be made required.
135-
private _platform?: Platform) {
135+
// @deletion-target 7.0.0 `_platform` and `_overlayContainer` parameters to be made required.
136+
private _platform?: Platform,
137+
private _overlayContainer?: OverlayContainer) {
136138
this.setOrigin(connectedTo);
137139
}
138140

@@ -169,6 +171,7 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
169171
*/
170172
apply(): void {
171173
// We shouldn't do anything if the strategy was disposed or we're on the server.
174+
// @deletion-target 7.0.0 Remove `_platform` null check once it's guaranteed to be defined.
172175
if (this._isDisposed || (this._platform && !this._platform.isBrowser)) {
173176
return;
174177
}
@@ -839,6 +842,18 @@ export class FlexibleConnectedPositionStrategy implements PositionStrategy {
839842
overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect);
840843
}
841844

845+
// @deletion-target 7.0.0 Currently the `_overlayContainer` is optional in order to avoid a
846+
// breaking change. The null check here can be removed once the `_overlayContainer` becomes
847+
// a required parameter.
848+
let virtualKeyboardOffset = this._overlayContainer ?
849+
this._overlayContainer.getContainerElement().getBoundingClientRect().top : 0;
850+
851+
// Normally this would be zero, however when the overlay is attached to an input (e.g. in an
852+
// autocomplete), mobile browsers will shift everything in order to put the input in the middle
853+
// of the screen and to make space for the virtual keyboard. We need to account for this offset,
854+
// otherwise our positioning will be thrown off.
855+
overlayPoint.y -= virtualKeyboardOffset;
856+
842857
// We want to set either `top` or `bottom` based on whether the overlay wants to appear
843858
// above or below the origin and the direction in which the element will expand.
844859
if (position.overlayY === 'bottom') {

src/cdk/overlay/position/overlay-position-builder.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {ConnectedPositionStrategy} from './connected-position-strategy';
1414
import {FlexibleConnectedPositionStrategy} from './flexible-connected-position-strategy';
1515
import {GlobalPositionStrategy} from './global-position-strategy';
1616
import {Platform} from '@angular/cdk/platform';
17+
import {OverlayContainer} from '../overlay-container';
1718

1819

1920
/** Builder for overlay position strategy. */
@@ -22,8 +23,9 @@ export class OverlayPositionBuilder {
2223
constructor(
2324
private _viewportRuler: ViewportRuler,
2425
@Inject(DOCUMENT) private _document: any,
25-
// @deletion-target 7.0.0 `_platform` parameter to be made required.
26-
@Optional() private _platform?: Platform) { }
26+
// @deletion-target 7.0.0 `_platform` and `_overlayContainer` parameters to be made required.
27+
@Optional() private _platform?: Platform,
28+
@Optional() private _overlayContainer?: OverlayContainer) { }
2729

2830
/**
2931
* Creates a global position strategy.
@@ -55,7 +57,7 @@ export class OverlayPositionBuilder {
5557
*/
5658
flexibleConnectedTo(elementRef: ElementRef | HTMLElement): FlexibleConnectedPositionStrategy {
5759
return new FlexibleConnectedPositionStrategy(elementRef, this._viewportRuler, this._document,
58-
this._platform);
60+
this._platform, this._overlayContainer);
5961
}
6062

6163
}

0 commit comments

Comments
 (0)