mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-07 23:16:52 +08:00
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:
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 [
|
||||
<ion-backdrop
|
||||
onClick={this.backdropClick.bind(this)}
|
||||
class='popover-backdrop'
|
||||
/>,
|
||||
<div class={wrapperClasses} style={wrapperStyle}>
|
||||
<div class={wrapperClasses}>
|
||||
<div class='popover-arrow'/>
|
||||
<div class='popover-content'>
|
||||
<div class='popover-viewport'>
|
||||
@ -327,7 +200,7 @@ export interface PopoverOptions {
|
||||
enableBackdropDismiss?: boolean;
|
||||
translucent?: boolean;
|
||||
enterAnimation?: AnimationBuilder;
|
||||
exitAnimation?: AnimationBuilder;
|
||||
leavenimation?: AnimationBuilder;
|
||||
cssClass?: string;
|
||||
ev: Event;
|
||||
}
|
||||
@ -359,3 +232,4 @@ export {
|
||||
mdEnterAnimation as mdPopoverEnterAnimation,
|
||||
mdLeaveAnimation as mdPopoverLeaveAnimation
|
||||
};
|
||||
|
||||
|
||||
@ -10,11 +10,11 @@
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-page>
|
||||
<ion-page class="ion-page">
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<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-button>
|
||||
</ion-buttons>
|
||||
@ -34,7 +34,7 @@
|
||||
<ion-footer>
|
||||
<ion-toolbar color="primary">
|
||||
<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-button>
|
||||
</ion-buttons>
|
||||
@ -115,4 +115,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user