chore(): refactor popover animation

* chore(): refactor popover animation

* docs(): move doc info

* fix(): params on one line

* docs(): remove leftover
This commit is contained in:
Mike Hartington
2017-12-01 09:43:55 -08:00
committed by GitHub
parent 1c45777064
commit 5a85df9b08
6 changed files with 237 additions and 143 deletions

View File

@ -1,7 +1,6 @@
import { Component, Listen, Method } from '@stencil/core'; import { Component, Listen, Method } from '@stencil/core';
import { Popover, PopoverEvent, PopoverOptions } from '../../index'; import { Popover, PopoverEvent, PopoverOptions } from '../../index';
@Component({ @Component({
tag: 'ion-popover-controller' tag: 'ion-popover-controller'
}) })
@ -10,6 +9,10 @@ export class PopoverController {
private popoverResolves: {[popoverId: string]: Function} = {}; private popoverResolves: {[popoverId: string]: Function} = {};
private popovers: Popover[] = []; private popovers: Popover[] = [];
/**
* Create a popover component instance
* @param opts Options when creating a new popover instance
*/
@Method() @Method()
create(opts?: PopoverOptions) { create(opts?: PopoverOptions) {
// create ionic's wrapping ion-popover component // create ionic's wrapping ion-popover component

View File

@ -1,5 +1,24 @@
# ion-popover-controller # 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.
<!-- Auto Generated Below --> <!-- Auto Generated Below -->

View File

@ -1,20 +1,141 @@
import { Animation } from '../../../index'; import { Animation } from '../../../index';
/** /**
* iOS Popover Enter Animation * 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 baseAnimation = new Animation();
const backdropAnimation = new Animation(); const backdropAnimation = new Animation();
backdropAnimation.addElement(baseElm.querySelector('.popover-backdrop')); backdropAnimation.addElement(baseElm.querySelector('.popover-backdrop'));
backdropAnimation.fromTo('opacity', 0.01, 0.08); 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 return baseAnimation
.addElement(baseElm) .addElement(baseElm)
.easing('ease') .easing('ease')
.duration(100) .duration(100)
.add(backdropAnimation); .add(backdropAnimation)
.add(wrapperAnimation);
} }
const POPOVER_IOS_BODY_PADDING = 5;

View File

@ -1,20 +1,97 @@
import { Animation } from '../../../index'; import { Animation } from '../../../index';
/** /**
* Md Popover Enter Animation * 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 baseAnimation = new Animation();
const backdropAnimation = new Animation(); const backdropAnimation = new Animation();
backdropAnimation.addElement(baseElm.querySelector('.popover-backdrop')); backdropAnimation.addElement(baseElm.querySelector('.popover-backdrop'));
backdropAnimation.fromTo('opacity', 0.01, 0.08); 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 return baseAnimation
.addElement(baseElm) .addElement(baseElm)
.easing('cubic-bezier(0.36,0.66,0.04,1)') .easing('cubic-bezier(0.36,0.66,0.04,1)')
.duration(300) .duration(300)
.add(backdropAnimation); .add(backdropAnimation)
.add(wrapperAnimation)
.add(contentAnimation)
.add(viewportAnimation);
} }
const POPOVER_MD_BODY_PADDING = 12;

View File

@ -21,8 +21,6 @@ import mdLeaveAnimation from './animations/md.leave';
export class Popover { export class Popover {
private animation: Animation; private animation: Animation;
@State() positioned: boolean = false;
@Element() private el: HTMLElement; @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) { private _present(resolve: Function) {
if (this.animation) { if (this.animation) {
this.animation.destroy(); this.animation.destroy();
@ -211,13 +88,12 @@ export class Popover {
// build the animation and kick it off // 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; this.animation = animation;
animation.onFinish((a: any) => { animation.onFinish((a: any) => {
a.destroy(); a.destroy();
this.componentDidEnter(); this.componentDidEnter();
this.positionPopover();
resolve(); resolve();
}).play(); }).play();
}); });
@ -295,17 +171,14 @@ export class Popover {
render() { render() {
const ThisComponent = this.component; const ThisComponent = this.component;
const wrapperClasses = createThemedClasses(this.mode, this.color, 'popover-wrapper'); const wrapperClasses = createThemedClasses(this.mode, this.color, 'popover-wrapper');
const wrapperStyle = this.positioned ? { 'opacity' : '1' } : {};
return [ return [
<ion-backdrop <ion-backdrop
onClick={this.backdropClick.bind(this)} onClick={this.backdropClick.bind(this)}
class='popover-backdrop' class='popover-backdrop'
/>, />,
<div class={wrapperClasses} style={wrapperStyle}> <div class={wrapperClasses}>
<div class='popover-arrow'/> <div class='popover-arrow'/>
<div class='popover-content'> <div class='popover-content'>
<div class='popover-viewport'> <div class='popover-viewport'>
@ -327,7 +200,7 @@ export interface PopoverOptions {
enableBackdropDismiss?: boolean; enableBackdropDismiss?: boolean;
translucent?: boolean; translucent?: boolean;
enterAnimation?: AnimationBuilder; enterAnimation?: AnimationBuilder;
exitAnimation?: AnimationBuilder; leavenimation?: AnimationBuilder;
cssClass?: string; cssClass?: string;
ev: Event; ev: Event;
} }
@ -359,3 +232,4 @@ export {
mdEnterAnimation as mdPopoverEnterAnimation, mdEnterAnimation as mdPopoverEnterAnimation,
mdLeaveAnimation as mdPopoverLeaveAnimation mdLeaveAnimation as mdPopoverLeaveAnimation
}; };

View File

@ -10,11 +10,11 @@
<body> <body>
<ion-app> <ion-app>
<ion-page> <ion-page class="ion-page">
<ion-header> <ion-header>
<ion-toolbar color="primary"> <ion-toolbar color="primary">
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-button onclick="presentPopover('profile-page', event)"> <ion-button onclick="presentPopover({component: 'profile-page', ev: event })">
<ion-icon slot="icon-only" name="person"></ion-icon> <ion-icon slot="icon-only" name="person"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
@ -34,7 +34,7 @@
<ion-footer> <ion-footer>
<ion-toolbar color="primary"> <ion-toolbar color="primary">
<ion-buttons slot="end"> <ion-buttons slot="end">
<ion-button onclick="presentPopover('profile-page', event)"> <ion-button onclick="presentPopover({component: 'profile-page', ev: event })">
<ion-icon slot="icon-only" name="person"></ion-icon> <ion-icon slot="icon-only" name="person"></ion-icon>
</ion-button> </ion-button>
</ion-buttons> </ion-buttons>
@ -115,4 +115,4 @@
</script> </script>
</body> </body>
</html> </html>