Skip to content

Commit

Permalink
fix(componentpositions): getBoundingClientRect() iOS fix
Browse files Browse the repository at this point in the history
fix #59
  • Loading branch information
Ondrej Base committed May 7, 2020
1 parent b9314b4 commit 211f46f
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 17 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@commitlint/config-conventional": "^8.3.4",
"@ima/core": "17.0.1",
"@ima/helpers": "^17.4.0",
"@ima/plugin-useragent": "^2.0.1",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babelify": "10.0.0",
Expand Down Expand Up @@ -94,7 +95,8 @@
},
"peerDependencies": {
"react": "16.x",
"@ima/core": "17.x"
"@ima/core": "17.x",
"@ima/plugin-useragent": "2.x"
},
"engines": {
"npm": ">=4 <6"
Expand Down
93 changes: 78 additions & 15 deletions src/ComponentPositions.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
import { UserAgent } from '@ima/plugin-useragent';

/**
* A bounding client rectangle.
* @typedef {{
* top: number,
* left: number,
* width: number,
* height: number
* }} BoundingClientRect
*/

/**
* Component positions helper.
*/
export default class ComponentPositions {
static get $dependencies() {
return ['$Window'];
return ['$Window', UserAgent];
}

/**
* Initializes the helper.
*
* @param {ima.window.Window} window
* @param {UserAgent} userAgent
*/
constructor(window) {
constructor(window, userAgent) {
/**
* @property _window
* @type {ima.window.Window}
*/
this._window = window;

/**
* @type {UserAgent}
*/
this._userAgent = userAgent;
}

/**
Expand All @@ -40,7 +57,7 @@ export default class ComponentPositions {
/**
* Returns percent of visibility defined area in window viewport.
*
* @param {{top: number, left: number, width: number, height: number}} elmRect
* @param {BoundingClientRect} elmRect
* @return {number} The percent of visibility.
*/
getPercentOfVisibility(elmRect) {
Expand Down Expand Up @@ -71,11 +88,11 @@ export default class ComponentPositions {
}

/**
* Returns intersection rectangle of two defined reactangles.
* Returns an intersection rectangle of two defined reactangles.
*
* @param {{top: number, left: number, width: number, height: number}} rect1
* @param {{top: number, left: number, width: number, height: number}} rect2
* @return {{top: number, left: number, width: number, height: number}} The intersection reactangle.
* @param {BoundingClientRect} rect1
* @param {BoundingClientRect} rect2
* @return {BoundingClientRect} The intersection rectangle.
*/
getRectsIntersection(rect1, rect2) {
let top = this.getNumberFromRange(rect2.top, rect1.top, rect1.height);
Expand All @@ -97,8 +114,8 @@ export default class ComponentPositions {
}

/**
* Returns number from defined range, if number is not in defined range return min
* or max depends on number.
* Returns number from defined range, if number is not in defined range return
* min or max depends on number.
*
* @param {number} number
* @param {number} min
Expand All @@ -112,7 +129,7 @@ export default class ComponentPositions {
/**
* Returns window viewport rect.
*
* @return {{top: number, left: number, width: number, height: number}}
* @return {BoundingClientRect}
*/
getWindowViewportRect() {
let win = this._window.getWindow();
Expand Down Expand Up @@ -142,13 +159,13 @@ export default class ComponentPositions {
}

/**
* Returns the size of an element and its position relative to the viewport and
* add extended value to returned rect.
* Returns the size of an element and its position relative to the viewport
* and add extended value to returned rect.
*
* @param {Element} element
* @param {{width: number, height: number}} size
* @param {number} extended
* @return {{top: number, left: number, width: number, height: number}}
* @return {BoundingClientRect}
*/
getBoundingClientRect(
element,
Expand Down Expand Up @@ -182,6 +199,52 @@ export default class ComponentPositions {
height: (clientRect.height || height || 0 / width || 0) + 2 * extended
};

return elmRectStyle;
return this._fixBoundingClientRectOnIOS(elmRectStyle);
}

/**
* Applies a fix for iOS 8+ bug, where overscroll messes up
* getBoundingClientRect()'s top value on all iOS webkit based devices:
* https://github.com/lionheart/openradar-mirror/issues/6233
*
* @param {BoundingClientRect} boundingClientRect A bounding client rectangle.
* @return {BoundingClientRect} A fixed bounding client rectangle.
*/
_fixBoundingClientRectOnIOS(boundingClientRect) {
if (this._userAgent.getOSFamily() !== 'iOS') {
return boundingClientRect;
}

const window = this._window.getWindow();
const maxScrollHeight = this._getMaxScrollHeight();
const rect = Object.assign({}, boundingClientRect);

if (window.scrollY < 0 && rect.top > 0) {
rect.top += window.scrollY;
} else if (window.scrollY > maxScrollHeight && rect.top < 0) {
rect.top += window.scrollY - maxScrollHeight;
}

return rect;
}

/**
* Returns maximum available scroll height (minus viewport height).
*
* @returns {number}
*/
_getMaxScrollHeight() {
const window = this._window.getWindow();
const document = this._window.getDocument();

return (
Math.max(
document.body.scrollHeight || 0,
document.body.offsetHeight || 0,
document.documentElement.clientHeight || 0,
document.documentElement.offsetHeight || 0,
document.documentElement.scrollHeight || 0
) - window.innerHeight
);
}
}
65 changes: 64 additions & 1 deletion src/__tests__/ComponentPositionsSpec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { UserAgent } from '@ima/plugin-useragent';
import { toMockedInstance } from 'to-mock';

import ComponentPositions from '../ComponentPositions';

import _window from '../mocks/window';

describe('ComponentPositions', () => {
const mockedUserAgent = toMockedInstance(UserAgent, {
getOSFamily: () => 'Windows'
});

let windowViewportRect = {
top: 0,
left: 0,
Expand All @@ -20,7 +27,7 @@ describe('ComponentPositions', () => {
let componentPositions = null;

beforeEach(() => {
componentPositions = new ComponentPositions(_window);
componentPositions = new ComponentPositions(_window, mockedUserAgent);
});

it('should return window viewport', () => {
Expand Down Expand Up @@ -123,4 +130,60 @@ describe('ComponentPositions', () => {
});
});
});

describe('getBoundingClientRect method (iOS fix)', () => {
beforeEach(() => {
spyOn(mockedUserAgent, 'getOSFamily').and.returnValue('iOS');
spyOn(_window, 'getDocument').and.returnValue({
body: { scrollHeight: 3000 },
documentElement: {}
});
});

it('should return a rectangle with fixed top on iOS', () => {
spyOn(_window, 'getWindow').and.returnValue({
innerHeight: 800,
scrollY: -100
});

const element = {
getBoundingClientRect: () => ({
top: 205,
left: 361,
width: 700,
height: 700
})
};

expect(componentPositions.getBoundingClientRect(element, {})).toEqual({
top: 105,
left: 361,
width: 700,
height: 700
});
});

it('should return a rectangle with fixed top on iOS for top < 0', () => {
spyOn(_window, 'getWindow').and.returnValue({
innerHeight: 800,
scrollY: 2300
});

const element = {
getBoundingClientRect: () => ({
top: -70,
left: 361,
width: 700,
height: 700
})
};

expect(componentPositions.getBoundingClientRect(element, {})).toEqual({
top: 30,
left: 361,
width: 700,
height: 700
});
});
});
});

0 comments on commit 211f46f

Please sign in to comment.