-
Notifications
You must be signed in to change notification settings - Fork 13.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(popover): adjust position to account for approximate safe area #29068
base: main
Are you sure you want to change the base?
Changes from all commits
0d7497a
c4918b9
ee626e0
21b285a
530e723
6503c60
69969e0
525a25f
cfac073
e97811a
6fd0e78
fec3b0b
6693737
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { expect } from '@playwright/test'; | ||
import { configs, test } from '@utils/test/playwright'; | ||
import { configs, test, Viewports } from '@utils/test/playwright'; | ||
|
||
/** | ||
* This behavior does not vary across modes/directions. | ||
|
@@ -33,5 +33,45 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => | |
|
||
expect(box.y > 0).toBe(true); | ||
}); | ||
|
||
test('should account for vertical safe area approximation in portrait mode', async ({ page }) => { | ||
await page.goto('/src/components/popover/test/adjustment', config); | ||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); | ||
|
||
await page.mouse.click(0, 0); | ||
await ionPopoverDidPresent.next(); | ||
|
||
const popoverContent = page.locator('ion-popover .popover-content'); | ||
const box = (await popoverContent.boundingBox())!; | ||
|
||
/** | ||
* The safe area approximation should move the y position by 5% | ||
* of the screen height. We use 10px as the threshold to give | ||
* wiggle room and help prevent flakiness. | ||
*/ | ||
expect(box.x < 10).toBe(true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These should be screenshot tests according to our best practices guide. (same as below test) |
||
expect(box.y > 10).toBe(true); | ||
}); | ||
|
||
test('should account for vertical and horizontal safe area approximation in landscape mode', async ({ page }) => { | ||
await page.goto('/src/components/popover/test/adjustment', config); | ||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); | ||
|
||
await page.setViewportSize(Viewports.tablet.landscape); | ||
|
||
await page.mouse.click(0, 0); | ||
await ionPopoverDidPresent.next(); | ||
|
||
const popoverContent = page.locator('ion-popover .popover-content'); | ||
const box = (await popoverContent.boundingBox())!; | ||
|
||
/** | ||
* The safe area approximation should move the y position by 5% | ||
* of the screen height. We use 10px as the threshold to give | ||
* wiggle room and help prevent flakiness. | ||
*/ | ||
expect(box.x > 10).toBe(true); | ||
expect(box.y > 10).toBe(true); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import { win } from '@utils/browser'; | ||
import { getElementRoot, raf } from '@utils/helpers'; | ||
|
||
import type { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from './popover-interface'; | ||
|
@@ -814,7 +815,6 @@ export const calculateWindowAdjustment = ( | |
bodyHeight: number, | ||
contentWidth: number, | ||
contentHeight: number, | ||
safeAreaMargin: number, | ||
contentOriginX: string, | ||
contentOriginY: string, | ||
triggerCoordinates?: ReferenceCoordinates, | ||
|
@@ -837,32 +837,72 @@ export const calculateWindowAdjustment = ( | |
const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0; | ||
let addPopoverBottomClass = false; | ||
|
||
/** | ||
* Approximate the safe area margins. Getting exact values would necessitate | ||
* using window.getComputedStyle(), which is very expensive, so we use "close | ||
* enough" values for now. | ||
* | ||
* 5% is derived from the iPhone 14 top safe area margin (47pt / 844 pt ~= 0.05). | ||
* Source: https://useyourloaf.com/blog/iphone-14-screen-sizes/ | ||
* | ||
* TODO(FW-5982): Investigate a more robust solution that uses the actual | ||
* safe area margins through alternate means. | ||
*/ | ||
let horizontalSafeAreaApprox = 0; | ||
let verticalSafeAreaApprox = 0; | ||
if (win?.matchMedia !== undefined) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
verticalSafeAreaApprox = win.innerHeight * 0.05; | ||
|
||
/** | ||
* We only want to check horizontal safe area on landscape. | ||
* Most devices do not have horizontal safe area margins in | ||
* portrait mode, so enforcing it would lead to popovers | ||
* being misaligned with the trigger when we don't want them | ||
* to move. | ||
*/ | ||
if (win.matchMedia('(orientation: landscape)').matches) { | ||
horizontalSafeAreaApprox = win.innerWidth * 0.05; | ||
} | ||
averyjohnston marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Adjust popover so it does not | ||
* go off the left of the screen. | ||
*/ | ||
if (left < bodyPadding + safeAreaMargin) { | ||
left = bodyPadding; | ||
if (left < bodyPadding + horizontalSafeAreaApprox) { | ||
left = bodyPadding + horizontalSafeAreaApprox; | ||
checkSafeAreaLeft = true; | ||
originX = 'left'; | ||
/** | ||
* Adjust popover so it does not | ||
* go off the right of the screen. | ||
*/ | ||
} else if (contentWidth + bodyPadding + left + safeAreaMargin > bodyWidth) { | ||
} else if (contentWidth + bodyPadding + left + horizontalSafeAreaApprox > bodyWidth) { | ||
checkSafeAreaRight = true; | ||
left = bodyWidth - contentWidth - bodyPadding; | ||
left = bodyWidth - contentWidth - bodyPadding - horizontalSafeAreaApprox; | ||
originX = 'right'; | ||
} | ||
|
||
/** | ||
* Ensure the popover doesn't sit above the safe area approxmation. | ||
* If popover is on the left or right of the trigger, we should not | ||
* adjust top margins. | ||
*/ | ||
if (side === 'top' || side === 'bottom') { | ||
top = Math.max(top, verticalSafeAreaApprox + bodyPadding); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we write a test for this? |
||
} | ||
|
||
/** | ||
* Adjust popover so it does not | ||
* go off the top of the screen. | ||
* If popover is on the left or the right of | ||
* the trigger, then we should not adjust top | ||
* margins. | ||
*/ | ||
if (triggerTop + triggerHeight + contentHeight > bodyHeight && (side === 'top' || side === 'bottom')) { | ||
if ( | ||
triggerTop + triggerHeight + contentHeight + verticalSafeAreaApprox > bodyHeight && | ||
(side === 'top' || side === 'bottom') | ||
) { | ||
if (triggerTop - contentHeight > 0) { | ||
/** | ||
* While we strive to align the popover with the trigger | ||
|
@@ -875,6 +915,12 @@ export const calculateWindowAdjustment = ( | |
* it is not right up against the edge of the screen. | ||
*/ | ||
top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1)); | ||
|
||
/** | ||
* Ensure the popover doesn't sit below the safe area approxmation. | ||
*/ | ||
top = Math.min(top, bodyHeight - verticalSafeAreaApprox - contentHeight - bodyPadding); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we write a test for this? |
||
|
||
arrowTop = top + contentHeight; | ||
originY = 'bottom'; | ||
addPopoverBottomClass = true; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While removing these looks like a step backwards at a glance, they weren't actually working as intended because the
checkSafeAreaLeft/Right
variables were based on the hardcoded 0/25px guess at the margins. I've removed them to ensure the MD and iOS safe area behavior lines up as closely as possible.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I understand: Since we are approximating the safe area bounds we no longer need these checks since it's already handled elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right; the adjustment is all done in
calculateWindowAdjustment
now.