diff --git a/packages/core/src/components/popover-controller/popover-controller.tsx b/packages/core/src/components/popover-controller/popover-controller.tsx
index b31ee9f7c7..0d01deef0f 100644
--- a/packages/core/src/components/popover-controller/popover-controller.tsx
+++ b/packages/core/src/components/popover-controller/popover-controller.tsx
@@ -1,7 +1,6 @@
import { Component, Listen, Method } from '@stencil/core';
import { Popover, PopoverEvent, PopoverOptions } from '../../index';
-
@Component({
tag: 'ion-popover-controller'
})
@@ -10,6 +9,10 @@ export class PopoverController {
private popoverResolves: {[popoverId: string]: Function} = {};
private popovers: Popover[] = [];
+ /**
+ * Create a popover component instance
+ * @param opts Options when creating a new popover instance
+ */
@Method()
create(opts?: PopoverOptions) {
// create ionic's wrapping ion-popover component
diff --git a/packages/core/src/components/popover-controller/readme.md b/packages/core/src/components/popover-controller/readme.md
index 295bacf0cf..ae674c1e3f 100644
--- a/packages/core/src/components/popover-controller/readme.md
+++ b/packages/core/src/components/popover-controller/readme.md
@@ -1,5 +1,24 @@
# ion-popover-controller
+A Popover is a dialog that appears on top of the current page.
+It can be used for anything, but generally it is used for overflow
+actions that don't fit in the navigation bar.
+
+### Creating
+A popover can be created by calling the `create` method. The view
+to display in the popover should be passed as the first argument.
+Any data to pass to the popover view can optionally be passed in
+the second argument. Options for the popover can optionally be
+passed in the third argument. See the [create](#create) method
+below for all available options.
+
+### Presenting
+To present a popover, call the `present` method on a PopoverController instance.
+In order to position the popover relative to the element clicked, a click event
+needs to be passed into the options of the the `present` method. If the event
+is not passed, the popover will be positioned in the center of the current
+view. See the [usage](#usage) section for an example of passing this event.
+
diff --git a/packages/core/src/components/popover/animations/ios.enter.ts b/packages/core/src/components/popover/animations/ios.enter.ts
index 5991a3b55c..574b2eb0bc 100644
--- a/packages/core/src/components/popover/animations/ios.enter.ts
+++ b/packages/core/src/components/popover/animations/ios.enter.ts
@@ -1,20 +1,141 @@
import { Animation } from '../../../index';
-
/**
* iOS Popover Enter Animation
*/
-export default function iosEnterAnimation(Animation: Animation, baseElm: HTMLElement): Animation {
+export default function iosEnterAnimation(Animation: Animation, baseElm: HTMLElement, ev?: Event): Animation {
+ let originY = 'top';
+ let originX = 'left';
+
+ let contentEl = baseElm.querySelector('.popover-content') as HTMLElement;
+ let contentDimentions = contentEl.getBoundingClientRect();
+ let contentWidth = contentDimentions.width;
+ let contentHeight = contentDimentions.height;
+
+ let bodyWidth = window.innerWidth;
+ let bodyHeight = window.innerHeight;
+
+ // If ev was passed, use that for target element
+ let targetDim =
+ ev && ev.target && (ev.target as HTMLElement).getBoundingClientRect();
+
+ let targetTop =
+ targetDim && 'top' in targetDim
+ ? targetDim.top
+ : bodyHeight / 2 - contentHeight / 2;
+ let targetLeft =
+ targetDim && 'left' in targetDim ? targetDim.left : bodyWidth / 2;
+ let targetWidth = (targetDim && targetDim.width) || 0;
+ let targetHeight = (targetDim && targetDim.height) || 0;
+
+ let arrowEl = baseElm.querySelector('.popover-arrow') as HTMLElement;
+
+ let arrowDim = arrowEl.getBoundingClientRect();
+ let arrowWidth = arrowDim.width;
+ let arrowHeight = arrowDim.height;
+
+ if (!targetDim) {
+ arrowEl.style.display = 'none';
+ }
+
+ let arrowCSS = {
+ top: targetTop + targetHeight,
+ left: targetLeft + targetWidth / 2 - arrowWidth / 2
+ };
+
+ let popoverCSS: { top: any; left: any } = {
+ top: targetTop + targetHeight + (arrowHeight - 1),
+ left: targetLeft + targetWidth / 2 - contentWidth / 2
+ };
+
+ // If the popover left is less than the padding it is off screen
+ // to the left so adjust it, else if the width of the popover
+ // exceeds the body width it is off screen to the right so adjust
+ //
+ let checkSafeAreaLeft = false;
+ let checkSafeAreaRight = false;
+
+ // If the popover left is less than the padding it is off screen
+ // to the left so adjust it, else if the width of the popover
+ // exceeds the body width it is off screen to the right so adjust
+ // 25 is a random/arbitrary number. It seems to work fine for ios11
+ // and iPhoneX. Is it perfect? No. Does it work? Yes.
+ if (popoverCSS.left < POPOVER_IOS_BODY_PADDING + 25) {
+ checkSafeAreaLeft = true;
+ popoverCSS.left = POPOVER_IOS_BODY_PADDING;
+ } else if (
+ contentWidth + POPOVER_IOS_BODY_PADDING + popoverCSS.left + 25 >
+ bodyWidth
+ ) {
+ // Ok, so we're on the right side of the screen,
+ // but now we need to make sure we're still a bit further right
+ // cus....notchurally... Again, 25 is random. It works tho
+ checkSafeAreaRight = true;
+ popoverCSS.left = bodyWidth - contentWidth - POPOVER_IOS_BODY_PADDING;
+ originX = 'right';
+ }
+
+ // make it pop up if there's room above
+ if (
+ targetTop + targetHeight + contentHeight > bodyHeight &&
+ targetTop - contentHeight > 0
+ ) {
+ arrowCSS.top = targetTop - (arrowHeight + 1);
+ popoverCSS.top = targetTop - contentHeight - (arrowHeight - 1);
+ baseElm.className = baseElm.className + ' popover-bottom';
+ originY = 'bottom';
+ // If there isn't room for it to pop up above the target cut it off
+ } else if (targetTop + targetHeight + contentHeight > bodyHeight) {
+ contentEl.style.bottom = POPOVER_IOS_BODY_PADDING + '%';
+ }
+
+ arrowEl.style.top = arrowCSS.top + 'px';
+ arrowEl.style.left = arrowCSS.left + 'px';
+
+ contentEl.style.top = popoverCSS.top + 'px';
+ contentEl.style.left = popoverCSS.left + 'px';
+
+ if (checkSafeAreaLeft) {
+ if (CSS.supports('left', 'constant(safe-area-inset-left)')) {
+ contentEl.style.left = `calc(${
+ popoverCSS.left
+ }px + constant(safe-area-inset-left)`;
+ } else if (CSS.supports('left', 'env(safe-area-inset-left)')) {
+ contentEl.style.left = `calc(${
+ popoverCSS.left
+ }px + env(safe-area-inset-left)`;
+ }
+ }
+
+ if (checkSafeAreaRight) {
+ if (CSS.supports('right', 'constant(safe-area-inset-right)')) {
+ contentEl.style.left = `calc(${
+ popoverCSS.left
+ }px - constant(safe-area-inset-right)`;
+ } else if (CSS.supports('right', 'env(safe-area-inset-right)')) {
+ contentEl.style.left = `calc(${
+ popoverCSS.left
+ }px - env(safe-area-inset-right)`;
+ }
+ }
+
+ contentEl.style.transformOrigin = originY + ' ' + originX;
+
const baseAnimation = new Animation();
const backdropAnimation = new Animation();
backdropAnimation.addElement(baseElm.querySelector('.popover-backdrop'));
-
backdropAnimation.fromTo('opacity', 0.01, 0.08);
+ const wrapperAnimation = new Animation();
+ wrapperAnimation.addElement(baseElm.querySelector('.popover-wrapper'));
+ wrapperAnimation.fromTo('opacity', 0.01, 1);
+
return baseAnimation
.addElement(baseElm)
.easing('ease')
.duration(100)
- .add(backdropAnimation);
+ .add(backdropAnimation)
+ .add(wrapperAnimation);
}
+const POPOVER_IOS_BODY_PADDING = 5;
diff --git a/packages/core/src/components/popover/animations/md.enter.ts b/packages/core/src/components/popover/animations/md.enter.ts
index fed85cd121..7a8249740d 100644
--- a/packages/core/src/components/popover/animations/md.enter.ts
+++ b/packages/core/src/components/popover/animations/md.enter.ts
@@ -1,20 +1,97 @@
import { Animation } from '../../../index';
-
/**
* Md Popover Enter Animation
*/
-export default function mdEnterAnimation(Animation: Animation, baseElm: HTMLElement): Animation {
+export default function mdEnterAnimation(Animation: Animation, baseElm: HTMLElement, ev?: Event): Animation {
+ let originY = 'top';
+ let originX = 'left';
+
+ let contentEl = baseElm.querySelector('.popover-content') as HTMLElement;
+ let contentDimentions = contentEl.getBoundingClientRect();
+ let contentWidth = contentDimentions.width;
+ let contentHeight = contentDimentions.height;
+
+ let bodyWidth = window.innerWidth;
+ let bodyHeight = window.innerHeight;
+
+ // If ev was passed, use that for target element
+ let targetDim =
+ ev && ev.target && (ev.target as HTMLElement).getBoundingClientRect();
+
+ let targetTop =
+ targetDim && 'top' in targetDim
+ ? targetDim.top
+ : bodyHeight / 2 - contentHeight / 2;
+
+ let targetLeft =
+ targetDim && 'left' in targetDim
+ ? targetDim.left
+ : bodyWidth / 2 - contentWidth / 2;
+
+ let targetHeight = (targetDim && targetDim.height) || 0;
+
+ let popoverCSS: { top: any; left: any } = {
+ top: targetTop,
+ left: targetLeft
+ };
+
+ // If the popover left is less than the padding it is off screen
+ // to the left so adjust it, else if the width of the popover
+ // exceeds the body width it is off screen to the right so adjust
+ if (popoverCSS.left < POPOVER_MD_BODY_PADDING) {
+ popoverCSS.left = POPOVER_MD_BODY_PADDING;
+ } else if (
+ contentWidth + POPOVER_MD_BODY_PADDING + popoverCSS.left >
+ bodyWidth
+ ) {
+ popoverCSS.left = bodyWidth - contentWidth - POPOVER_MD_BODY_PADDING;
+ originX = 'right';
+ }
+
+ // If the popover when popped down stretches past bottom of screen,
+ // make it pop up if there's room above
+ if (
+ targetTop + targetHeight + contentHeight > bodyHeight &&
+ targetTop - contentHeight > 0
+ ) {
+ popoverCSS.top = targetTop - contentHeight;
+ baseElm.className = baseElm.className + ' popover-bottom';
+ originY = 'bottom';
+ // If there isn't room for it to pop up above the target cut it off
+ } else if (targetTop + targetHeight + contentHeight > bodyHeight) {
+ contentEl.style.bottom = POPOVER_MD_BODY_PADDING + 'px';
+ }
+
+ contentEl.style.top = popoverCSS.top + 'px';
+ contentEl.style.left = popoverCSS.left + 'px';
+ contentEl.style.transformOrigin = originY + ' ' + originX;
+
const baseAnimation = new Animation();
const backdropAnimation = new Animation();
backdropAnimation.addElement(baseElm.querySelector('.popover-backdrop'));
-
backdropAnimation.fromTo('opacity', 0.01, 0.08);
+ const wrapperAnimation = new Animation();
+ wrapperAnimation.addElement(baseElm.querySelector('.popover-wrapper'));
+ wrapperAnimation.fromTo('opacity', 0.01, 1);
+
+ const contentAnimation = new Animation();
+ contentAnimation.addElement(baseElm.querySelector('.popover-content'));
+ contentAnimation.fromTo('scale', 0.001, 1);
+
+ const viewportAnimation = new Animation();
+ viewportAnimation.addElement(baseElm.querySelector('.popover-viewport'));
+ viewportAnimation.fromTo('opacity', 0.01, 1);
+
return baseAnimation
.addElement(baseElm)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.duration(300)
- .add(backdropAnimation);
+ .add(backdropAnimation)
+ .add(wrapperAnimation)
+ .add(contentAnimation)
+ .add(viewportAnimation);
}
+const POPOVER_MD_BODY_PADDING = 12;
diff --git a/packages/core/src/components/popover/popover.tsx b/packages/core/src/components/popover/popover.tsx
index 0da0b39e07..b41aacbd02 100644
--- a/packages/core/src/components/popover/popover.tsx
+++ b/packages/core/src/components/popover/popover.tsx
@@ -21,8 +21,6 @@ import mdLeaveAnimation from './animations/md.leave';
export class Popover {
private animation: Animation;
- @State() positioned: boolean = false;
-
@Element() private el: HTMLElement;
/**
@@ -78,127 +76,6 @@ export class Popover {
});
}
-
- private positionPopover() {
- const props = POPOVER_POSITION_PROPERTIES[this.mode];
- console.debug('Position popover', this.el, this.ev, props);
-
- // Declare the popover elements
- let contentEl = this.el.querySelector('.popover-content') as HTMLElement;
- let arrowEl = this.el.querySelector('.popover-arrow') as HTMLElement;
-
- // If no event was passed, hide the arrow
- if (!this.ev) {
- arrowEl.style.display = 'none';
- }
-
- // Set the default transform origin direction
- let origin = {
- y: 'top',
- x: 'left'
- };
-
- // Popover content width and height
- const popover = {
- width: contentEl.getBoundingClientRect().width,
- height: contentEl.getBoundingClientRect().height
- };
-
- // Window body width and height
- // TODO need to check if portrait/landscape?
- const body = {
- width: window.screen.width,
- height: window.screen.height
- };
-
- // If ev was passed, use that for target element
- let targetDim = this.ev && this.ev.target && (this.ev.target as HTMLElement).getBoundingClientRect();
-
- // The target is the object that dispatched the event that was passed
- let target = {
- top: (targetDim && 'top' in targetDim) ? targetDim.top : (body.height / 2) - (popover.height / 2),
- left: (targetDim && 'left' in targetDim) ? targetDim.left : (body.width / 2) - (popover.width / 2),
- width: targetDim && targetDim.width || 0,
- height: targetDim && targetDim.height || 0
- };
-
- // If the popover should be centered to the target
- if (props.centerTarget) {
- target.left = (targetDim && 'left' in targetDim) ? targetDim.left : (body.width / 2);
- }
-
- // The arrow that shows above the popover on iOS
- let arrowDim = arrowEl.getBoundingClientRect();
-
- const arrow = {
- width: arrowDim.width,
- height: arrowDim.height
- };
-
- let arrowCSS = {
- top: target.top + target.height,
- left: target.left + (target.width / 2) - (arrow.width / 2)
- };
-
- let popoverCSS = {
- top: target.top + target.height + (arrow.height - 1),
- left: target.left
- };
-
- // If the popover should be centered to the target
- if (props.centerTarget) {
- popoverCSS.left = target.left + (target.width / 2) - (popover.width / 2);
- }
-
- // If the popover left is less than the padding it is off screen
- // to the left so adjust it, else if the width of the popover
- // exceeds the body width it is off screen to the right so adjust
- if (popoverCSS.left < props.padding) {
- popoverCSS.left = props.padding;
- } else if (popover.width + props.padding + popoverCSS.left > body.width) {
- popoverCSS.left = body.width - popover.width - props.padding;
- origin.x = 'right';
- }
-
- // If the popover when popped down stretches past bottom of screen,
- // make it pop up if there's room above
- if (this.showFromBottom(target, popover, body)) {
- this.el.className = this.el.className + ' popover-bottom';
- origin.y = 'bottom';
-
- popoverCSS.top = target.top - popover.height;
-
- if (props.showArrow) {
- arrowCSS.top = target.top - (arrow.height + 1);
- popoverCSS.top = target.top - popover.height - (arrow.height - 1);
- }
-
- // If the popover exceeds the viewport then cut the bottom off
- } else if (this.exceedsViewport(target, popover, body)) {
- contentEl.style.bottom = props.padding + props.unit;
- }
-
- arrowEl.style.top = arrowCSS.top + 'px';
- arrowEl.style.left = arrowCSS.left + 'px';
-
- contentEl.style.top = popoverCSS.top + 'px';
- contentEl.style.left = popoverCSS.left + 'px';
-
- contentEl.style.transformOrigin = origin.y + ' ' + origin.x;
-
- // Since the transition starts before styling is done we
- // want to wait for the styles to apply before showing the wrapper
- this.positioned = true;
- }
-
- private showFromBottom(target: any, popover: any, body: any): boolean {
- return target.top + target.height + popover.height > body.height && target.top - popover.height > 0;
- }
-
- private exceedsViewport(target: any, popover: any, body: any): boolean {
- return target.top + target.height + popover.height > body.height;
- }
-
private _present(resolve: Function) {
if (this.animation) {
this.animation.destroy();
@@ -211,13 +88,12 @@ export class Popover {
// build the animation and kick it off
- this.animationCtrl.create(animationBuilder, this.el).then(animation => {
+ this.animationCtrl.create(animationBuilder, this.el, this.ev).then(animation => {
this.animation = animation;
animation.onFinish((a: any) => {
a.destroy();
this.componentDidEnter();
- this.positionPopover();
resolve();
}).play();
});
@@ -295,17 +171,14 @@ export class Popover {
render() {
const ThisComponent = this.component;
-
const wrapperClasses = createThemedClasses(this.mode, this.color, 'popover-wrapper');
- const wrapperStyle = this.positioned ? { 'opacity' : '1' } : {};
-
return [