Skip to content

Commit

Permalink
fix(DOM): iOS safari pinch zooming fix
Browse files Browse the repository at this point in the history
This is an amendment from @floating-ui adapted to @popperjs and flow

fix floating-ui#1121 re floating-ui#1576
  • Loading branch information
Mateusz Daniluk committed Aug 6, 2022
1 parent 593aefe commit aa63fe1
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 50 deletions.
57 changes: 34 additions & 23 deletions src/dom-utils/getBoundingClientRect.js
@@ -1,38 +1,49 @@
// @flow
import type { ClientRectObject, VirtualElement } from '../types';
import { isHTMLElement } from './instanceOf';
import { isElement, isHTMLElement} from './instanceOf';
import { round } from '../utils/math';
import getWindow from './getWindow';
import isLayoutViewport from './isLayoutViewport';

export default function getBoundingClientRect(
element: Element | VirtualElement,
includeScale: boolean = false
includeScale: boolean = false,
isFixedStrategy: boolean = false
): ClientRectObject {
const rect = element.getBoundingClientRect();
const clientRect = element.getBoundingClientRect();
let scaleX = 1;
let scaleY = 1;

if (isHTMLElement(element) && includeScale) {
const offsetHeight = element.offsetHeight;
const offsetWidth = element.offsetWidth;

// Do not attempt to divide by 0, otherwise we get `Infinity` as scale
// Fallback to 1 in case both values are `0`
if (offsetWidth > 0) {
scaleX = round(rect.width) / offsetWidth || 1;
}
if (offsetHeight > 0) {
scaleY = round(rect.height) / offsetHeight || 1;
}
if (includeScale && isHTMLElement(element)) {
scaleX =
(element: HTMLElement).offsetWidth > 0
? round(clientRect.width) / (element: HTMLElement).offsetWidth || 1
: 1;
scaleY =
(element: HTMLElement).offsetHeight > 0
? round(clientRect.height) / (element: HTMLElement).offsetHeight || 1
: 1;
}

const {visualViewport} = isElement(element) ? getWindow(element) : window;
const addVisualOffsets = !isLayoutViewport() && isFixedStrategy;

const x =
(clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) /
scaleX;
const y =
(clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) /
scaleY;
const width = clientRect.width / scaleX;
const height = clientRect.height / scaleY;
return {
width: rect.width / scaleX,
height: rect.height / scaleY,
top: rect.top / scaleY,
right: rect.right / scaleX,
bottom: rect.bottom / scaleY,
left: rect.left / scaleX,
x: rect.left / scaleX,
y: rect.top / scaleY,
width,
height,
top: y,
right: x + width,
bottom: y + height,
left: x,
x,
y
};
}
23 changes: 14 additions & 9 deletions src/dom-utils/getClippingRect.js
@@ -1,5 +1,5 @@
// @flow
import type { ClientRectObject } from '../types';
import type { ClientRectObject, PositioningStrategy } from '../types';
import type { Boundary, RootBoundary } from '../enums';
import { viewport } from '../enums';
import getViewportRect from './getViewportRect';
Expand All @@ -16,8 +16,11 @@ import getNodeName from './getNodeName';
import rectToClientRect from '../utils/rectToClientRect';
import { max, min } from '../utils/math';

function getInnerBoundingClientRect(element: Element) {
const rect = getBoundingClientRect(element);
function getInnerBoundingClientRect(
element: Element,
strategy: PositioningStrategy
) {
const rect = getBoundingClientRect(element, false, strategy === 'fixed');

rect.top = rect.top + element.clientTop;
rect.left = rect.left + element.clientLeft;
Expand All @@ -33,12 +36,13 @@ function getInnerBoundingClientRect(element: Element) {

function getClientRectFromMixedType(
element: Element,
clippingParent: Element | RootBoundary
clippingParent: Element | RootBoundary,
strategy: PositioningStrategy,
): ClientRectObject {
return clippingParent === viewport
? rectToClientRect(getViewportRect(element))
? rectToClientRect(getViewportRect(element, strategy))
: isElement(clippingParent)
? getInnerBoundingClientRect(clippingParent)
? getInnerBoundingClientRect(clippingParent, strategy)
: rectToClientRect(getDocumentRect(getDocumentElement(element)));
}

Expand Down Expand Up @@ -72,7 +76,8 @@ function getClippingParents(element: Element): Array<Element> {
export default function getClippingRect(
element: Element,
boundary: Boundary,
rootBoundary: RootBoundary
rootBoundary: RootBoundary,
strategy: PositioningStrategy
): ClientRectObject {
const mainClippingParents =
boundary === 'clippingParents'
Expand All @@ -82,15 +87,15 @@ export default function getClippingRect(
const firstClippingParent = clippingParents[0];

const clippingRect = clippingParents.reduce((accRect, clippingParent) => {
const rect = getClientRectFromMixedType(element, clippingParent);
const rect = getClientRectFromMixedType(element, clippingParent, strategy);

accRect.top = max(rect.top, accRect.top);
accRect.right = min(rect.right, accRect.right);
accRect.bottom = min(rect.bottom, accRect.bottom);
accRect.left = max(rect.left, accRect.left);

return accRect;
}, getClientRectFromMixedType(element, firstClippingParent));
}, getClientRectFromMixedType(element, firstClippingParent, strategy));

clippingRect.width = clippingRect.right - clippingRect.left;
clippingRect.height = clippingRect.bottom - clippingRect.top;
Expand Down
3 changes: 2 additions & 1 deletion src/dom-utils/getCompositeRect.js
Expand Up @@ -30,7 +30,8 @@ export default function getCompositeRect(
const documentElement = getDocumentElement(offsetParent);
const rect = getBoundingClientRect(
elementOrVirtualElement,
offsetParentIsScaled
offsetParentIsScaled,
isFixed
);

let scroll = { scrollLeft: 0, scrollTop: 0 };
Expand Down
23 changes: 8 additions & 15 deletions src/dom-utils/getViewportRect.js
Expand Up @@ -2,8 +2,13 @@
import getWindow from './getWindow';
import getDocumentElement from './getDocumentElement';
import getWindowScrollBarX from './getWindowScrollBarX';
import isLayoutViewport from './isLayoutViewport';
import type { PositioningStrategy } from '../types';

export default function getViewportRect(element: Element) {
export default function getViewportRect(
element: Element,
strategy: PositioningStrategy
) {
const win = getWindow(element);
const html = getDocumentElement(element);
const visualViewport = win.visualViewport;
Expand All @@ -13,25 +18,13 @@ export default function getViewportRect(element: Element) {
let x = 0;
let y = 0;

// NB: This isn't supported on iOS <= 12. If the keyboard is open, the popper
// can be obscured underneath it.
// Also, `html.clientHeight` adds the bottom bar height in Safari iOS, even
// if it isn't open, so if this isn't available, the popper will be detected
// to overflow the bottom of the screen too early.
if (visualViewport) {
width = visualViewport.width;
height = visualViewport.height;

// Uses Layout Viewport (like Chrome; Safari does not currently)
// In Chrome, it returns a value very close to 0 (+/-) but contains rounding
// errors due to floating point numbers, so we need to check precision.
// Safari returns a number <= 0, usually < -1 when pinch-zoomed
const layoutViewport = isLayoutViewport();

// Feature detection fails in mobile emulation mode in Chrome.
// Math.abs(win.innerWidth / visualViewport.scale - visualViewport.width) <
// 0.001
// Fallback here: "Not Safari" userAgent
if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
if (layoutViewport || (!layoutViewport && strategy === 'fixed')) {
x = visualViewport.offsetLeft;
y = visualViewport.offsetTop;
}
Expand Down
25 changes: 25 additions & 0 deletions src/dom-utils/isLayoutViewport.js
@@ -0,0 +1,25 @@
// @flow
type Navigator = Navigator & { userAgentData?: NavigatorUAData };

interface NavigatorUAData {
brands: Array<{brand: string; version: string}>;
mobile: boolean;
platform: string;
}

function getUAString(): string {

const uaData = (navigator: Navigator).userAgentData;

if (uaData?.brands) {
return uaData.brands
.map((item) => `${item.brand}/${item.version}`)
.join(' ');
}

return navigator.userAgent;
}

export default function isLayoutViewport() {
return !/^((?!chrome|android).)*safari/i.test(getUAString());
}
7 changes: 5 additions & 2 deletions src/utils/detectOverflow.js
@@ -1,5 +1,5 @@
// @flow
import type { State, SideObject, Padding } from '../types';
import type { State, SideObject, Padding, PositioningStrategy } from '../types';
import type { Placement, Boundary, RootBoundary, Context } from '../enums';
import getClippingRect from '../dom-utils/getClippingRect';
import getDocumentElement from '../dom-utils/getDocumentElement';
Expand All @@ -23,6 +23,7 @@ import expandToHashMap from './expandToHashMap';
// eslint-disable-next-line import/no-unused-modules
export type Options = {
placement: Placement,
strategy: PositioningStrategy,
boundary: Boundary,
rootBoundary: RootBoundary,
elementContext: Context,
Expand All @@ -36,6 +37,7 @@ export default function detectOverflow(
): SideObject {
const {
placement = state.placement,
strategy = state.strategy,
boundary = clippingParents,
rootBoundary = viewport,
elementContext = popper,
Expand All @@ -59,7 +61,8 @@ export default function detectOverflow(
? element
: element.contextElement || getDocumentElement(state.elements.popper),
boundary,
rootBoundary
rootBoundary,
strategy,
);

const referenceClientRect = getBoundingClientRect(state.elements.reference);
Expand Down

0 comments on commit aa63fe1

Please sign in to comment.