-
Notifications
You must be signed in to change notification settings - Fork 13.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(toast): allow custom positioning relative to specific element (#…
…28248) Issue number: resolves #17499 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Currently, there isn't a way to position toasts such that they don't overlap navigation elements such as headers, footers, and FABs. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> Added the new `positionAnchor` property, which specifies an element that the toast's position should be anchored to. While the name can be tweaked, we should take care to keep the relation between it and the `position` property clear. The `position` acts as a sort of "origin" point, and the toast is moved from there to sit near the chosen anchor element. This is important because it helps clarify why the toast sits above the anchor for `position="bottom"` and vice versa. I chose not to rename the `position` prop itself to avoid breaking changes. Docs PR: ionic-team/ionic-docs#3158 ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> --------- Co-authored-by: ionitron <[email protected]> Co-authored-by: Liam DeBeasi <[email protected]>
- Loading branch information
1 parent
01167fc
commit 897ff6f
Showing
36 changed files
with
376 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { win } from '@utils/browser'; | ||
import { printIonWarning } from '@utils/logging'; | ||
import type { Mode } from 'src/interface'; | ||
|
||
import type { ToastAnimationPosition, ToastPosition } from '../toast-interface'; | ||
|
||
/** | ||
* Calculate the CSS top and bottom position of the toast, to be used | ||
* as starting points for the animation keyframes. | ||
* | ||
* Note that MD animates bottom-positioned toasts using style.bottom, | ||
* which calculates from the bottom edge of the screen, while iOS uses | ||
* translateY, which calculates from the top edge of the screen. This | ||
* is why the bottom calculates differ slightly between modes. | ||
* | ||
* @param position The value of the toast's position prop. | ||
* @param positionAnchor The element the toast should be anchored to, | ||
* if applicable. | ||
* @param mode The toast component's mode (md, ios, etc). | ||
* @param toast A reference to the toast element itself. | ||
*/ | ||
export function getAnimationPosition( | ||
position: ToastPosition, | ||
positionAnchor: HTMLElement | undefined, | ||
mode: Mode, | ||
toast: HTMLElement | ||
): ToastAnimationPosition { | ||
/** | ||
* Start with a predefined offset from the edge the toast will be | ||
* positioned relative to, whether on the screen or anchor element. | ||
*/ | ||
let offset: number; | ||
if (mode === 'md') { | ||
offset = 8; | ||
} else { | ||
offset = position === 'top' ? 10 : -10; | ||
} | ||
|
||
/** | ||
* If positionAnchor is defined, add in the distance from the target | ||
* screen edge to the target anchor edge. For position="top", the | ||
* bottom anchor edge is targeted. For position="bottom", the top | ||
* anchor edge is targeted. | ||
*/ | ||
if (positionAnchor && win) { | ||
warnIfAnchorIsHidden(positionAnchor, toast); | ||
|
||
const box = positionAnchor.getBoundingClientRect(); | ||
if (position === 'top') { | ||
offset += box.bottom; | ||
} else if (position === 'bottom') { | ||
/** | ||
* Just box.top is the distance from the top edge of the screen | ||
* to the top edge of the anchor. We want to calculate from the | ||
* bottom edge of the screen instead. | ||
*/ | ||
if (mode === 'md') { | ||
offset += win.innerHeight - box.top; | ||
} else { | ||
offset -= win.innerHeight - box.top; | ||
} | ||
} | ||
|
||
/** | ||
* We don't include safe area here because that should already be | ||
* accounted for when checking the position of the anchor. | ||
*/ | ||
return { | ||
top: `${offset}px`, | ||
bottom: `${offset}px`, | ||
}; | ||
} else { | ||
return { | ||
top: `calc(${offset}px + var(--ion-safe-area-top, 0px))`, | ||
bottom: | ||
mode === 'md' | ||
? `calc(${offset}px + var(--ion-safe-area-bottom, 0px))` | ||
: `calc(${offset}px - var(--ion-safe-area-bottom, 0px))`, | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* If the anchor element is hidden, getBoundingClientRect() | ||
* will return all 0s for it, which can cause unexpected | ||
* results in the position calculation when animating. | ||
*/ | ||
function warnIfAnchorIsHidden(positionAnchor: HTMLElement, toast: HTMLElement) { | ||
if (positionAnchor.offsetParent === null) { | ||
printIonWarning( | ||
'The positionAnchor element for ion-toast was found in the DOM, but appears to be hidden. This may lead to unexpected positioning of the toast.', | ||
toast | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
<!DOCTYPE html> | ||
<html lang="en" dir="ltr"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<title>Toast - positionAnchor</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 type="module" src="../../../../../dist/ionic/ionic.esm.js"></script> | ||
|
||
<style> | ||
html { | ||
--ion-safe-area-top: 30px; | ||
--ion-safe-area-bottom: 30px; | ||
} | ||
</style> | ||
</head> | ||
|
||
<body> | ||
<ion-app> | ||
<ion-header id="header"> | ||
<ion-toolbar> | ||
<ion-title>Toast - positionAnchor</ion-title> | ||
</ion-toolbar> | ||
</ion-header> | ||
|
||
<ion-content class="ion-padding"> | ||
<ion-button id="headerAnchor">Anchor to Header</ion-button> | ||
<ion-button id="footerAnchor">Anchor to Footer</ion-button> | ||
<ion-button id="middleAnchor">Anchor to Header (Middle Position)</ion-button> | ||
<ion-button id="headerElAnchor">Anchor to Header (Element Ref)</ion-button> | ||
<ion-button id="hiddenElAnchor">Anchor to Hidden Element</ion-button> | ||
|
||
<ion-toast | ||
id="headerToast" | ||
trigger="headerAnchor" | ||
position="top" | ||
position-anchor="header" | ||
message="Hello World" | ||
duration="2000" | ||
></ion-toast> | ||
<ion-toast | ||
id="footerToast" | ||
trigger="footerAnchor" | ||
position="bottom" | ||
position-anchor="footer" | ||
message="Hello World" | ||
duration="2000" | ||
></ion-toast> | ||
<ion-toast | ||
id="middleToast" | ||
trigger="middleAnchor" | ||
position="middle" | ||
position-anchor="header" | ||
message="Hello World" | ||
duration="2000" | ||
></ion-toast> | ||
<ion-toast | ||
id="headerElToast" | ||
trigger="headerElAnchor" | ||
position="top" | ||
message="Hello World" | ||
duration="2000" | ||
></ion-toast> | ||
<ion-toast | ||
id="hiddenElToast" | ||
trigger="hiddenElAnchor" | ||
position="bottom" | ||
position-anchor="hiddenEl" | ||
message="Hello World" | ||
duration="2000" | ||
></ion-toast> | ||
|
||
<div id="hiddenEl" style="display: none">Shh I'm hiding</div> | ||
</ion-content> | ||
|
||
<ion-footer id="footer"> | ||
<ion-toolbar> | ||
<ion-title>Footer</ion-title> | ||
</ion-toolbar> | ||
</ion-footer> | ||
</ion-app> | ||
|
||
<script> | ||
const headerElToast = document.querySelector('#headerElToast'); | ||
const header = document.querySelector('ion-header'); | ||
headerElToast.positionAnchor = header; | ||
</script> | ||
</body> | ||
</html> |
56 changes: 56 additions & 0 deletions
56
core/src/components/toast/test/position-anchor/toast.e2e.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { expect } from '@playwright/test'; | ||
import { configs, test } from '@utils/test/playwright'; | ||
|
||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { | ||
test.describe(title('toast: positionAnchor'), () => { | ||
test.beforeEach(async ({ page }) => { | ||
await page.goto('/src/components/toast/test/position-anchor', config); | ||
|
||
/** | ||
* We need to screenshot the whole page to ensure the toasts are positioned | ||
* correctly, but we don't need much extra white space between the header | ||
* and footer. | ||
*/ | ||
await page.setViewportSize({ | ||
width: 425, | ||
height: 425, | ||
}); | ||
}); | ||
|
||
test('should place top-position toast underneath anchor', async ({ page }) => { | ||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent'); | ||
|
||
await page.click('#headerAnchor'); | ||
await ionToastDidPresent.next(); | ||
|
||
await expect(page).toHaveScreenshot(screenshot(`toast-header-anchor`)); | ||
}); | ||
|
||
test('should place bottom-position toast above anchor', async ({ page }) => { | ||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent'); | ||
|
||
await page.click('#footerAnchor'); | ||
await ionToastDidPresent.next(); | ||
|
||
await expect(page).toHaveScreenshot(screenshot(`toast-footer-anchor`)); | ||
}); | ||
|
||
test('should ignore anchor for middle-position toast', async ({ page }) => { | ||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent'); | ||
|
||
await page.click('#middleAnchor'); | ||
await ionToastDidPresent.next(); | ||
|
||
await expect(page).toHaveScreenshot(screenshot(`toast-middle-anchor`)); | ||
}); | ||
|
||
test('should correctly anchor toast when using an element reference', async ({ page }) => { | ||
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent'); | ||
|
||
await page.click('#headerElAnchor'); | ||
await ionToastDidPresent.next(); | ||
|
||
await expect(page).toHaveScreenshot(screenshot(`toast-header-el-anchor`)); | ||
}); | ||
}); | ||
}); |
Binary file added
BIN
+17.7 KB
...chor/toast.e2e.ts-snapshots/toast-footer-anchor-ios-ltr-Mobile-Chrome-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+26.1 KB
...hor/toast.e2e.ts-snapshots/toast-footer-anchor-ios-ltr-Mobile-Firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+17 KB
...chor/toast.e2e.ts-snapshots/toast-footer-anchor-ios-ltr-Mobile-Safari-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+18.1 KB
...nchor/toast.e2e.ts-snapshots/toast-footer-anchor-md-ltr-Mobile-Chrome-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+29.8 KB
...chor/toast.e2e.ts-snapshots/toast-footer-anchor-md-ltr-Mobile-Firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+17.6 KB
...nchor/toast.e2e.ts-snapshots/toast-footer-anchor-md-ltr-Mobile-Safari-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16.2 KB
...chor/toast.e2e.ts-snapshots/toast-header-anchor-ios-ltr-Mobile-Chrome-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+23.3 KB
...hor/toast.e2e.ts-snapshots/toast-header-anchor-ios-ltr-Mobile-Firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.5 KB
...chor/toast.e2e.ts-snapshots/toast-header-anchor-ios-ltr-Mobile-Safari-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16.2 KB
...nchor/toast.e2e.ts-snapshots/toast-header-anchor-md-ltr-Mobile-Chrome-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25.2 KB
...chor/toast.e2e.ts-snapshots/toast-header-anchor-md-ltr-Mobile-Firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.6 KB
...nchor/toast.e2e.ts-snapshots/toast-header-anchor-md-ltr-Mobile-Safari-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16.2 KB
...r/toast.e2e.ts-snapshots/toast-header-el-anchor-ios-ltr-Mobile-Chrome-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+23.3 KB
.../toast.e2e.ts-snapshots/toast-header-el-anchor-ios-ltr-Mobile-Firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.5 KB
...r/toast.e2e.ts-snapshots/toast-header-el-anchor-ios-ltr-Mobile-Safari-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16.2 KB
...or/toast.e2e.ts-snapshots/toast-header-el-anchor-md-ltr-Mobile-Chrome-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25.2 KB
...r/toast.e2e.ts-snapshots/toast-header-el-anchor-md-ltr-Mobile-Firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.6 KB
...or/toast.e2e.ts-snapshots/toast-header-el-anchor-md-ltr-Mobile-Safari-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16.5 KB
...chor/toast.e2e.ts-snapshots/toast-middle-anchor-ios-ltr-Mobile-Chrome-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25.1 KB
...hor/toast.e2e.ts-snapshots/toast-middle-anchor-ios-ltr-Mobile-Firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16 KB
...chor/toast.e2e.ts-snapshots/toast-middle-anchor-ios-ltr-Mobile-Safari-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.9 KB
...nchor/toast.e2e.ts-snapshots/toast-middle-anchor-md-ltr-Mobile-Chrome-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+26.1 KB
...chor/toast.e2e.ts-snapshots/toast-middle-anchor-md-ltr-Mobile-Firefox-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.3 KB
...nchor/toast.e2e.ts-snapshots/toast-middle-anchor-md-ltr-Mobile-Safari-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.