From 5a85df9b086c9270af446046b74065a528b7e458 Mon Sep 17 00:00:00 2001 From: Mike Hartington Date: Fri, 1 Dec 2017 09:43:55 -0800 Subject: [PATCH] chore(): refactor popover animation * chore(): refactor popover animation * docs(): move doc info * fix(): params on one line * docs(): remove leftover --- .../popover-controller/popover-controller.tsx | 5 +- .../components/popover-controller/readme.md | 19 +++ .../popover/animations/ios.enter.ts | 129 ++++++++++++++++- .../components/popover/animations/md.enter.ts | 85 ++++++++++- .../core/src/components/popover/popover.tsx | 134 +----------------- .../components/popover/test/basic/index.html | 8 +- 6 files changed, 237 insertions(+), 143 deletions(-) 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 [ , -
+
@@ -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 }; + diff --git a/packages/core/src/components/popover/test/basic/index.html b/packages/core/src/components/popover/test/basic/index.html index 541fc19576..bfd2375c10 100644 --- a/packages/core/src/components/popover/test/basic/index.html +++ b/packages/core/src/components/popover/test/basic/index.html @@ -10,11 +10,11 @@ - + - + @@ -34,7 +34,7 @@ - + @@ -115,4 +115,4 @@ - + \ No newline at end of file