add more tests, fix onFinish functionality, being testing with nav transitions

This commit is contained in:
Liam DeBeasi
2019-07-17 16:20:40 -04:00
parent ec32f56182
commit a78ba2db53
12 changed files with 994 additions and 213 deletions

View File

@@ -953,13 +953,14 @@ export class Nav implements NavOutlet {
}
private onMove(stepValue: number) {
console.log('step', stepValue, this.sbAni);
if (this.sbAni) {
this.sbAni.progressStep(stepValue);
(this.sbAni as any).playStep(stepValue);
}
}
private onEnd(shouldComplete: boolean, stepValue: number, dur: number) {
if (this.sbAni) {
if (this.sbAni && this.sbAni.progressEnd) {
this.sbAni.progressEnd(shouldComplete, stepValue, dur);
}
}

View File

@@ -12,11 +12,14 @@ export interface Animation {
afterRemoveClasses: string[];
afterStylesValue: { [property: string]: any };
animationFinish(): void;
play(): Animation;
playStep(step: number): Animation;
pause(): Animation;
stop(): Animation;
destroy(): Animation;
progressStart(): Animation;
from(property: string, value: any): Animation;
to(property: string, value: any): Animation;
@@ -25,7 +28,7 @@ export interface Animation {
addAnimation(animationToADd: Animation | Animation[] | undefined | null): Animation;
addTarget(target: string): Animation;
addElement(el: Node | Node[] | NodeList | undefined | null): Animation;
addElement(el: Element | Element[] | Node | Node[] | NodeList | undefined | null): Animation;
iterations(iterations: number): Animation;
fill(fill: 'auto' | 'none' | 'forwards' | 'backwards' | 'both' | undefined): Animation;
direction(direction: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse' | undefined): Animation;
@@ -159,6 +162,8 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
let webAnimations: any[] = [];
let onFinishCallback: any | undefined;
let numAnimationsRunning = 0;
/**
* Destroy this animation and all child animations.
*/
@@ -203,6 +208,8 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
element.style.removeProperty('animation-iteration-count');
element.style.removeProperty('animation-delay');
element.style.removeProperty('animation-play-state');
element.style.removeProperty('animation-fill-mode');
element.style.removeProperty('animation-direction');
});
}
};
@@ -394,7 +401,7 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
return generatePublicAPI();
};
const addElement = (el: Node | Node[] | NodeList | undefined | null): Animation => {
const addElement = (el: Element | Element[] | Node | Node[] | NodeList | undefined | null): Animation => {
if (el != null) {
const nodeList = el as NodeList;
if (nodeList.length >= 0) {
@@ -481,77 +488,84 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
}
};
const initializeAnimation = () => {
beforeAnimation();
const animationFinish = () => {
if (numAnimationsRunning === 0) { return; }
if (supportsWebAnimations()) {
elements.forEach((element, i) => {
const animation = element.animate(getKeyframes(), {
delay: getDelay(),
duration: getDuration(),
easing: getEasing(),
iterations: getIterations(),
fill: getFill(),
direction: getDirection()
});
numAnimationsRunning--;
if (i === 0) {
animation.onfinish = () => {
afterAnimation();
};
}
if (numAnimationsRunning === 0) {
afterAnimation();
}
animation.pause();
if (parentAnimation) {
parentAnimation.animationFinish();
}
};
webAnimations.push(animation);
});
} else {
if (!stylesheet) {
const initializeCSSAnimation = () => {
if (!stylesheet) {
stylesheet = createKeyframeStylesheet(generateKeyframeString(_name, _keyframes));
}
const animationDuration = getDuration();
const animationEasing = getEasing();
const animationIterationCount = getIterations();
const animationDelay = getDelay();
const animationFill = getFill();
const animationDirection = getDirection();
elements.forEach(element => {
element.style.setProperty('animation-name', _name || null);
element.style.setProperty('animation-duration', (getDuration() !== undefined) ? `${getDuration()}ms` : null);
element.style.setProperty('animation-timing-function', getEasing() || null);
element.style.setProperty('animation-delay', (getDelay() !== undefined) ? `${getDelay()}ms` : null);
element.style.setProperty('animation-fill-mode', getFill() || null);
element.style.setProperty('animation-direction', getDirection() || null);
elements.forEach(element => {
if (_name !== undefined) {
element.style.setProperty('animation-name', _name);
let iterationsCount = null;
if (getIterations() !== undefined) {
iterationsCount = (getIterations() === Infinity) ? 'infinite' : getIterations()!.toString();
}
if (animationDuration !== undefined) {
element.style.setProperty('animation-duration', `${animationDuration}ms`);
}
if (animationEasing !== undefined) {
element.style.setProperty('animation-timing-function', animationEasing);
}
if (animationIterationCount !== undefined) {
element.style.setProperty('animation-iteration-count', (animationIterationCount === Infinity) ? 'infinite' : animationIterationCount.toString());
}
if (animationDelay !== undefined) {
element.style.setProperty('animation-delay', `${animationDelay}ms`);
}
if (animationFill !== undefined) {
element.style.setProperty('animation-fill-mode', animationFill);
}
if (animationDirection !== undefined) {
element.style.setProperty('animation-direction', animationDirection);
}
element.style.setProperty('animation-directiteration-countion', iterationsCount);
});
animationEnd(elements[0], () => {
afterAnimation();
if (elements.length > 0) {
animationEnd(elements[0], () => {
animationFinish();
});
}
};
const initializeWebAnimation = () => {
elements.forEach(element => {
const animation = element.animate(getKeyframes(), {
delay: getDelay(),
duration: getDuration(),
easing: getEasing(),
iterations: getIterations(),
fill: getFill(),
direction: getDirection()
});
animation.pause();
webAnimations.push(animation);
});
if (webAnimations.length > 0) {
webAnimations[0].onfinish = () => {
animationFinish();
};
}
};
const initializeAnimation = () => {
beforeAnimation();
numAnimationsRunning = childAnimations.length + 1;
if (getKeyframes().length === 0) {
animationFinish();
} else {
if (supportsWebAnimations()) {
initializeWebAnimation();
} else {
initializeCSSAnimation();
}
}
initialized = true;
@@ -562,10 +576,7 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
animation.playStep(step);
});
if (!initialized) {
initializeAnimation();
}
progressStart();
pause();
if (getDuration() !== undefined) {
@@ -610,9 +621,7 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
animation.play();
});
if (!initialized) {
initializeAnimation();
}
progressStart(true);
if (supportsWebAnimations()) {
webAnimations.forEach(animation => {
@@ -663,7 +672,7 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
const to = (property: string, value: any): Animation => {
const keyframeValues = getKeyframes();
const lastFrame = keyframeValues[keyframes.length - 1];
const lastFrame = keyframeValues[keyframeValues.length - 1];
if (lastFrame != null && (lastFrame.offset === undefined || lastFrame.offset === 1)) {
lastFrame[property] = value;
@@ -686,6 +695,19 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
return from(property, fromValue).to(property, toValue);
};
const progressStart = (reset = false): Animation => {
if (initialized && reset) {
initialized = false;
cleanUpElements();
}
if (!initialized) {
initializeAnimation();
}
return generatePublicAPI();
};
const generatePublicAPI = (): Animation => {
return {
parentAnimation,
@@ -697,6 +719,9 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
afterAddClasses,
afterRemoveClasses,
afterStylesValue,
animationFinish,
from,
to,
fromTo,
@@ -733,6 +758,7 @@ export const createAnimation = (animationNameValue: string | undefined): Animati
beforeRemoveClass,
beforeAddClass,
onFinish,
progressStart
};
};

View File

@@ -102,7 +102,7 @@ describe('Animation Class', () => {
})
});
describe('keyframes()', () => {
describe('Animation Keyframes', () => {
let animation;
beforeEach(() => {
animation = createAnimation('my-animation');
@@ -275,8 +275,9 @@ describe('Animation Class', () => {
it('should get parent easing when child easing is not set', () => {
const childAnimation = createAnimation();
animation.addAnimation(childAnimation);
childAnimation.easing('linear');
animation
.addAnimation(childAnimation)
.easing('linear');
expect(childAnimation.getEasing()).toEqual('linear');
});
@@ -285,8 +286,9 @@ describe('Animation Class', () => {
const childAnimation = createAnimation();
childAnimation.easing('linear');
animation.addAnimation(childAnimation);
animation.easing('ease-in-out');
animation
.addAnimation(childAnimation)
.easing('ease-in-out');
expect(childAnimation.getEasing()).toEqual('linear');
});
@@ -297,8 +299,9 @@ describe('Animation Class', () => {
it('should get parent duration when child duration is not set', () => {
const childAnimation = createAnimation();
animation.addAnimation(childAnimation);
childAnimation.duration(500);
animation
.addAnimation(childAnimation)
.duration(500);
expect(childAnimation.getDuration()).toEqual(500);
});
@@ -307,8 +310,9 @@ describe('Animation Class', () => {
const childAnimation = createAnimation();
childAnimation.duration(500);
animation.addAnimation(childAnimation);
animation.duration(1000);
animation
.addAnimation(childAnimation)
.duration(1000);
expect(childAnimation.getDuration()).toEqual(500);
});
@@ -319,8 +323,9 @@ describe('Animation Class', () => {
it('should get parent delay when child delay is not set', () => {
const childAnimation = createAnimation();
animation.addAnimation(childAnimation);
childAnimation.delay(500);
animation
.addAnimation(childAnimation)
.delay(500);
expect(childAnimation.getDelay()).toEqual(500);
});
@@ -329,8 +334,9 @@ describe('Animation Class', () => {
const childAnimation = createAnimation();
childAnimation.delay(500);
animation.addAnimation(childAnimation);
animation.delay(1000);
animation
.addAnimation(childAnimation)
.delay(1000);
expect(childAnimation.getDelay()).toEqual(500);
});
@@ -341,8 +347,9 @@ describe('Animation Class', () => {
it('should get parent iterations when child iterations is not set', () => {
const childAnimation = createAnimation();
animation.addAnimation(childAnimation);
childAnimation.iterations(2);
animation
.addAnimation(childAnimation)
.iterations(2);
expect(childAnimation.getIterations()).toEqual(2);
});
@@ -351,10 +358,59 @@ describe('Animation Class', () => {
const childAnimation = createAnimation();
childAnimation.iterations(2);
animation.addAnimation(childAnimation);
animation.iterations(1);
animation
.addAnimation(childAnimation)
.iterations(1);
expect(childAnimation.getIterations()).toEqual(2);
});
it('should get undefined when fill not set', () => {
expect(animation.getFill()).toEqual(undefined);
});
it('should get parent fill when child fill is not set', () => {
const childAnimation = createAnimation();
animation
.addAnimation(childAnimation)
.fill('both');
expect(childAnimation.getFill()).toEqual('both');
});
it('should get prefer child fill over parent fill', () => {
const childAnimation = createAnimation();
childAnimation.fill('none');
animation
.addAnimation(childAnimation)
.fill('forwards');
expect(childAnimation.getFill()).toEqual('none');
});
it('should get undefined when direction not set', () => {
expect(animation.getDirection()).toEqual(undefined);
});
it('should get parent direction when child direction is not set', () => {
const childAnimation = createAnimation();
animation
.addAnimation(childAnimation)
.direction('alternate');
expect(childAnimation.getDirection()).toEqual('alternate');
});
it('should get prefer child direction over parent direction', () => {
const childAnimation = createAnimation();
childAnimation.direction('alternate-reverse');
animation
.addAnimation(childAnimation)
.direction('normal');
expect(childAnimation.getDirection()).toEqual('alternate-reverse');
});
})

View File

@@ -0,0 +1,28 @@
import { newE2EPage } from '@stencil/core/testing';
import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils';
test.only(`animation: basic`, async () => {
const page = await newE2EPage({ url: '/src/utils/animation/test/basic?ionic:_testing=true' });
const screenshotCompares = [];
screenshotCompares.push(await page.compareScreenshot());
const ANIMATION_FINISHED = 'onIonAnimationFinished';
const animationFinishedCount: any = { count: 0 };
await page.exposeFunction(ANIMATION_FINISHED, () => {
animationFinishedCount.count += 1;
});
const square = await page.$('.square-a');
await listenForEvent(page, 'ionAnimationFinished', square, ANIMATION_FINISHED);
await page.click('.play');
await page.waitForSelector('.play');
await waitForFunctionTestContext((payload: any) => {
return payload.animationFinishedCount.count === 1;
}, { animationFinishedCount });
screenshotCompares.push(await page.compareScreenshot());
});

View File

@@ -13,127 +13,42 @@
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { createAnimation } from '../../../../dist/collection/utils/animation/animation.js';
import { createGesture } from '../../../../dist/ionic/index-a098653b.js';
function clamp(val) {
if (val > 1) {
return 1;
} else if (val < 0) {
return 0;
}
return val;
}
const squareA = document.querySelectorAll('.square-a');
const squareB = document.querySelectorAll('.square-b');
const squareC = document.querySelectorAll('.square-c');
const track = document.querySelector('.track');
const cursor = document.querySelector('.track .cursor');
const squareA = document.querySelector('.square-a');
const rootAnimation = createAnimation();
const animationA = createAnimation('animation-a');
const animationB = createAnimation('animation-b');
const animationC = createAnimation('animation-c');
const gesture = createGesture({
el: track,
gestureName: 'drag',
gesturePriority: 100,
treshold: 5,
onStart: () => {},
onMove: (ev) => {
const start = ev.startX;
const width = track.clientWidth;
const lower = start;
const upper = width - start;
const current = clamp((ev.currentX - lower) / upper);
rootAnimation.playStep(current);
cursor.style.transform = `translateX(${ev.currentX - 10}px)`;
},
onEnd: (ev) => {
rootAnimation.playStep(0);
cursor.style.transform = `translateX(${0}px)`;
}
});
gesture.setDisabled(false);
animationA
rootAnimation
.addElement(squareA)
.duration(2000)
.delay(5000)
.duration(1000)
.easing('linear')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) rotate(45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
]);
{ background: 'rgba(255, 0, 0, 0.5)', offset: 0 },
{ background: 'rgba(0, 255, 0, 0.5)', offset: 0.33 },
{ background: 'rgba(0, 0, 255, 0.5)', offset: 0.66 },
{ background: 'rgba(255, 0, 0, 0.5)', offset: 1 }
])
.onFinish(() => {
const ev = new CustomEvent('ionAnimationFinished');
squareA.dispatchEvent(ev);
});
animationB
.addElement(squareB)
.duration(500)
.delay(2000)
.easing('ease-in-out')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(0.5) rotate(-45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
]);
animationC
.addElement(squareC)
.duration(2000)
.delay(500)
.easing('ease-in-out')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) skew(15deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 1 }
]);
rootAnimation.addAnimation([animationA, animationB, animationC]);
//rootAnimation.play();
document.querySelector('.play').addEventListener('click', () => {
rootAnimation.play();
});
</script>
<style>
.square {
width: 100px;
height: 100px;
background: rgba(0, 0, 255, 0.5);
background: rgba(255, 0, 0, 0.5);
text-align: center;
line-height: 100px;
margin-left: 25px;
margin-top: 25px;
margin-bottom: 25px;
}
.track {
width: 100%;
height: 100px;
background: rgba(0, 255, 0, 0.5);
text-align: center;
line-height: 100px;
user-select: none;
cursor: ew-resize;
position: relative;
}
.track .cursor {
pointer-events: none;
width: 10px;
height: 100px;
background: rgba(255, 0, 0, 0.5);
position: absolute;
top: 0;
}
</style>
</head>
@@ -147,21 +62,14 @@
<ion-content>
<div class="ion-padding">
<div class="track">
Drag along the track to animate the elements
<div class="cursor"></div>
</div>
<ion-button class="play">Play</ion-button>
<ion-button class="pause">Pause</ion-button>
<ion-button class="stop">Stop</ion-button>
<ion-button class="destroy">Destroy</ion-button>
<div class="square square-a">
Hello
</div>
<div class="square square-b">
Hello
</div>
<div class="square square-c">
Hello
</div>
</div>
</ion-content>
</ion-app>

View File

@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Animation - Basic</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { createAnimation } from '../../../../dist/collection/utils/animation/animation.js';
const squareA = document.querySelectorAll('.square-a');
const squareB = document.querySelectorAll('.square-b');
const squareC = document.querySelectorAll('.square-c');
const squareCText = document.querySelectorAll('.square-c .text');
const animationA = createAnimation('animation-a');
const animationB = createAnimation('animation-b');
const animationC = createAnimation('animation-c');
const animationCSubA = createAnimation('animation-c-sub-a');
const animationCSubB = createAnimation('animation-c-sub-b');
animationA
.addElement(squareA)
.duration(1000)
.easing('linear')
.iterations(1)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) rotate(45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
])
.beforeStyles({
'background': 'rgba(0, 0, 255, 0.5'
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
});
animationB
.addElement(squareB)
.duration(500)
.easing('ease-in-out')
.iterations(1)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(0.5) rotate(-45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
])
.beforeStyles({
'background': 'rgba(0, 0, 255, 0.5'
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
});
animationCSubA
.addElement(squareCText)
.duration(1000)
.delay(2000)
.keyframes([
{
offset: 0,
transform: 'scale(1)'
},
{
offset: 0.5,
transform: 'scale(1.5)'
},
{
offset: 1,
transform: 'scale(1)'
}
])
.fromTo('color', 'red', 'blue')
.onFinish(() => {
document.querySelector('li.animation-c-sub-a .status').innerText = 'DONE';
});
animationCSubB
.addElement(squareCText)
.duration(1000)
.delay(3500)
.fromTo('background', 'red', 'blue')
.onFinish(() => {
document.querySelector('li.animation-c-sub-b .status').innerText = 'DONE';
});
animationC
.addElement(squareC)
.duration(2000)
.easing('ease-in-out')
.iterations(1)
.keyframes([
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) skew(15deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 1 }
])
.beforeStyles({
'background': 'rgba(0, 0, 255, 0.5'
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
})
.addAnimation([animationCSubA, animationCSubB])
.onFinish(() => {
document.querySelector('li.animation-c .status').innerText = 'DONE';
});
animationA.onFinish(() => {
animationB.play();
document.querySelector('li.animation-a .status').innerText = 'DONE';
});
animationB.onFinish(() => {
animationC.play()
document.querySelector('li.animation-b .status').innerText = 'DONE';
});
document.querySelector('.play').addEventListener('click', () => {
animationA.play();
document.querySelectorAll('.status').forEach(status => {
status.innerText = '';
})
});
document.querySelector('.pause').addEventListener('click', () => {
animationA.pause();
});
document.querySelector('.destroy').addEventListener('click', () => {
animationA.destroy();
});
document.querySelector('.stop').addEventListener('click', () => {
animationA.stop();
document.querySelectorAll('.status').forEach(status => {
status.innerText = '';
})
});
</script>
<style>
.square {
width: 100px;
height: 100px;
background: rgba(0, 0, 255, 0.5);
text-align: center;
line-height: 100px;
margin-left: 25px;
margin-top: 25px;
margin-bottom: 25px;
}
.status-pane {
position: absolute;
top: 10px;
right: 10px;
width: 300px;
height: 400px;
padding-right: 10px;
background: rgba(0, 0, 0, 0.3);
}
li .status {
float: right;
}
</style>
</head>
<body
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Animations</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<ion-button class="play">Play</ion-button>
<ion-button class="pause">Pause</ion-button>
<ion-button class="stop">Stop</ion-button>
<ion-button class="destroy">Destroy</ion-button>
<div class="square square-a">
<div class="text">Hello</div>
</div>
<div class="square square-b">
<div class="text">Hello</div>
</div>
<div class="square square-c">
<div class="text">Hello</div>
</div>
<div class="status-pane">
<ul>
<li class="animation-a">Animation A<div class="status"></div></li>
</ul>
<ul>
<li class="animation-b">Animation B<div class="status"></div></li>
</ul>
<ul>
<li class="animation-c">Animation C<div class="status"></div></li>
<ul>
<li class="animation-c-sub-a">Animation C sub A<div class="status"></div></li>
<li class="animation-c-sub-b">Animation C sub B<div class="status"></div></li>
</ul>
</ul>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,61 @@
import { newE2EPage } from '@stencil/core/testing';
import { checkComponentModeClasses, checkModeClasses } from '../utils';
// This test is to loop through all components that should have
// specific classes added and test them
test('component: modes', async () => {
const page = await newE2EPage({
url: '/src/utils/test/modes?ionic:_testing=true'
});
// First test: .button class
// ----------------------------------------------------------------
// components that need to have the `button` class
// for use in styling by other components (`ion-buttons`)
// e.g. <ion-back-button class="button">
let tags = ['ion-button', 'ion-back-button', 'ion-menu-button'];
for (const tag of tags) {
const el = await page.find(tag);
expect(el).toHaveClass('button');
}
// Second test: .item class
// ----------------------------------------------------------------
// components that need to have the `item` class
// for use in styling by other components
// e.g. <ion-item-divider class="item">
tags = ['ion-item', 'ion-item-divider', 'ion-item-group'];
for (const tag of tags) {
const el = await page.find(tag);
expect(el).toHaveClass('item');
}
// Third test: .{component}-{mode} class
// ----------------------------------------------------------------
// components that need to have their tag name
// + mode as a class for internal styling
// e.g. <ion-card-content class="card-content-md">
tags = ['ion-card-content', 'ion-footer', 'ion-header', 'ion-infinite-scroll-content', 'ion-item-group', 'ion-item-options', 'ion-list', 'ion-picker', 'ion-refresher', 'ion-slides', 'ion-split-pane'];
for (const tag of tags) {
const el = await page.find(tag);
await checkComponentModeClasses(el);
}
// Fourth test: .{mode} class
// ----------------------------------------------------------------
// components that need to have the mode class
// added for external / user styling
// e.g. <ion-badge class="md">
tags = ['ion-action-sheet', 'ion-alert', 'ion-anchor', 'ion-app', 'ion-avatar', 'ion-back-button', 'ion-backdrop', 'ion-badge', 'ion-button', 'ion-buttons', 'ion-card-content', 'ion-card-header', 'ion-card-subtitle', 'ion-card-title', 'ion-card', 'ion-checkbox', 'ion-chip', 'ion-col', 'ion-content', 'ion-datetime', 'ion-fab', 'ion-fab-button', 'ion-fab-list', 'ion-footer', 'ion-grid', 'ion-header', 'ion-icon', 'ion-img', 'ion-infinite-scroll', 'ion-infinite-scroll-content', 'ion-input', 'ion-item', 'ion-item-divider', 'ion-item-group', 'ion-item-option', 'ion-item-options', 'ion-item-sliding', 'ion-label', 'ion-list', 'ion-list-header', 'ion-loading', 'ion-modal', 'ion-menu', 'ion-menu-button', 'ion-menu-toggle', 'ion-note', 'ion-picker', 'ion-picker-column', 'ion-popover', 'ion-progress-bar', 'ion-radio', 'ion-radio-group', 'ion-range', 'ion-refresher', 'ion-refresher-content', 'ion-reorder', 'ion-reorder-group', 'ion-ripple-effect', 'ion-row', 'ion-searchbar', 'ion-segment', 'ion-segment-button', 'ion-select', 'ion-select-option', 'ion-select-popover', 'ion-skeleton-text', 'ion-slide', 'ion-slides', 'ion-spinner', 'ion-split-pane', 'ion-tab-bar', 'ion-tab-button', 'ion-text', 'ion-textarea', 'ion-thumbnail', 'ion-title', 'ion-toast', 'ion-toggle', 'ion-toolbar'];
const globalMode = await page.evaluate(() => document.documentElement.getAttribute('mode'));
for (const tag of tags) {
await page.waitForSelector(tag);
const el = await page.find(tag);
await checkModeClasses(el, globalMode!);
}
});

View File

@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Animation - Basic</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
import { createAnimation } from '../../../../dist/collection/utils/animation/animation.js';
import { createGesture } from '../../../../dist/ionic/index-91289ad8.js';
function clamp(val) {
if (val > 1) {
return 1;
} else if (val < 0) {
return 0;
}
return val;
}
const squareA = document.querySelectorAll('.square-a');
const squareB = document.querySelectorAll('.square-b');
const squareC = document.querySelectorAll('.square-c');
const track = document.querySelector('.track');
const cursor = document.querySelector('.track .cursor');
const rootAnimation = createAnimation();
const animationA = createAnimation('animation-a');
const animationB = createAnimation('animation-b');
const animationC = createAnimation('animation-c');
const gesture = createGesture({
el: track,
gestureName: 'drag',
gesturePriority: 100,
treshold: 5,
onStart: () => {},
onMove: (ev) => {
const start = ev.startX;
const width = track.clientWidth;
const lower = start;
const upper = width - start;
const current = clamp((ev.currentX - lower) / upper);
rootAnimation.playStep(current);
cursor.style.transform = `translateX(${ev.currentX - 10}px)`;
},
onEnd: (ev) => {
rootAnimation.playStep(0);
cursor.style.transform = `translateX(${0}px)`;
}
});
gesture.setDisabled(false);
animationA
.addElement(squareA)
.duration(2000)
.delay(5000)
.easing('linear')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) rotate(45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
]);
animationB
.addElement(squareB)
.duration(500)
.delay(2000)
.easing('ease-in-out')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(0.5) rotate(-45deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) rotate(0deg)', opacity: 1, offset: 1 }
]);
animationC
.addElement(squareC)
.duration(2000)
.delay(500)
.easing('ease-in-out')
.iterations(Infinity)
.keyframes([
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 0 },
{ transform: 'scale(1.5) skew(15deg)', opacity: 0.5, offset: 0.5 },
{ transform: 'scale(1) skew(0deg)', opacity: 1, offset: 1 }
]);
rootAnimation.addAnimation([animationA, animationB, animationC]);
//rootAnimation.play();
</script>
<style>
.square {
width: 100px;
height: 100px;
background: rgba(0, 0, 255, 0.5);
text-align: center;
line-height: 100px;
margin-left: 25px;
margin-top: 25px;
margin-bottom: 25px;
}
.track {
width: 100%;
height: 100px;
background: rgba(0, 255, 0, 0.5);
text-align: center;
line-height: 100px;
user-select: none;
cursor: ew-resize;
position: relative;
}
.track .cursor {
pointer-events: none;
width: 10px;
height: 100px;
background: rgba(255, 0, 0, 0.5);
position: absolute;
top: 0;
}
</style>
</head>
<body
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Animations</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<div class="track">
Drag along the track to animate the elements
<div class="cursor"></div>
</div>
<div class="square square-a">
Hello
</div>
<div class="square square-b">
Hello
</div>
<div class="square square-c">
Hello
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -17,12 +17,15 @@
const squareA = document.querySelectorAll('.square-a');
const squareB = document.querySelectorAll('.square-b');
const squareC = document.querySelectorAll('.square-c');
const squareCText = document.querySelectorAll('.square-c .text');
const rootAnimation = createAnimation();
const animationA = createAnimation('animation-a');
const animationB = createAnimation('animation-b');
const animationC = createAnimation('animation-c');
const animationCSubA = createAnimation('animation-c-sub-a');
animationA
.addElement(squareA)
.duration(2000)
@@ -39,6 +42,9 @@
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
})
.onFinish(() => {
console.log('Animation A Done');
});
animationB
@@ -57,8 +63,11 @@
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
})
.onFinish(() => {
console.log('Animation B Done');
});
animationC
.addElement(squareC)
.duration(2000)
@@ -75,9 +84,27 @@
})
.afterStyles({
'background': 'rgba(0, 255, 0, 0.5)'
})
.onFinish(() => {
console.log('Animation C Done')
});
animationCSubA
.addElement(squareCText)
.duration(1000)
.delay(3000)
.fromTo('color', 'red', 'blue')
.onFinish(() => {
console.log('Animation CSubA Done');
});
animationC.addAnimation(animationCSubA);
rootAnimation.addAnimation([animationA, animationB, animationC]);
rootAnimation
.addAnimation([animationA, animationB, animationC])
.onFinish(() => {
console.log('Root Animation Done');
});
document.querySelector('.play').addEventListener('click', () => {
rootAnimation.play();
@@ -126,15 +153,15 @@
<ion-button class="destroy">Destroy</ion-button>
<div class="square square-a">
Hello
<div class="text">Hello</div>
</div>
<div class="square square-b">
Hello
<div class="text">Hello</div>
</div>
<div class="square square-c">
Hello
<div class="text">Hello</div>
</div>
</div>
</ion-content>

View File

@@ -77,18 +77,32 @@ const animation = async (animationBuilder: AnimationBuilder, opts: TransitionOpt
await waitForReady(opts, true);
const trans = await import('../animation').then(mod => mod.create(animationBuilder, opts.baseEl, opts));
trans;
fireWillEvents(opts.enteringEl, opts.leavingEl);
await playTransition(trans, opts);
const animation = ((opts.mode === 'ios')
? (await iosTransitionAnimation()).newIosTransitionAnimation(opts.baseEl, opts)
: (await mdTransitionAnimation()).newMdTransitionAnimation(opts) as any);
animation;
await playTransition(animation, opts);
animation.hasCompleted = true;
if (opts.progressCallback) {
opts.progressCallback(undefined);
}
if (trans.hasCompleted) {
if (animation.hasCompleted) {
fireDidEvents(opts.enteringEl, opts.leavingEl);
}
console.log('done!');
return {
hasCompleted: trans.hasCompleted,
animation: trans
hasCompleted: animation.hasCompleted,
animation
};
};
@@ -126,7 +140,7 @@ const notifyViewReady = async (viewIsReady: undefined | ((enteringEl: HTMLElemen
}
};
const playTransition = (trans: Animation, opts: TransitionOptions): Promise<Animation> => {
const playTransition = async (trans: any, opts: TransitionOptions): Promise<Animation> => {
const progressCallback = opts.progressCallback;
const promise = new Promise<Animation>(resolve => trans.onFinish(resolve));
@@ -135,7 +149,7 @@ const playTransition = (trans: Animation, opts: TransitionOptions): Promise<Anim
// this is a swipe to go back, just get the transition progress ready
// kick off the swipe animation start
trans.progressStart();
progressCallback(trans);
progressCallback(trans as any);
} else {
// only the top level transition should actually start "play"

View File

@@ -1,4 +1,5 @@
import { Animation } from '../../interface';
import { Animation as AnimationNew, createAnimation } from '../animation/animation';
import { TransitionOptions } from '../transition';
const DURATION = 500;
@@ -13,6 +14,212 @@ export const shadow = <T extends Element>(el: T): ShadowRoot | T => {
return el.shadowRoot || el;
};
export const newIosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptions): Promise<AnimationNew> => {
try {
const isRTL = (navEl.ownerDocument as any).dir === 'rtl';
const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%';
const OFF_LEFT = isRTL ? '33%' : '-33%';
const enteringEl = opts.enteringEl;
const leavingEl = opts.leavingEl;
const backDirection = (opts.direction === 'back');
const contentEl = enteringEl.querySelector(':scope > ion-content');
const headerEls = enteringEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
const enteringToolBarEls = enteringEl.querySelectorAll(':scope > ion-header > ion-toolbar');
const rootAnimation = createAnimation('ios-root-animation');
const enteringContentAnimation = createAnimation('ios-entering-content-animation');
rootAnimation
.addElement(enteringEl)
.duration(opts.duration || DURATION)
.easing(opts.easing || EASING)
.beforeRemoveClass('ion-page-invisible');
if (leavingEl && navEl) {
const navDecorAnimation = createAnimation('ios-decor-animation');
navDecorAnimation.addElement(navEl);
rootAnimation.addAnimation(navDecorAnimation);
}
if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) {
enteringContentAnimation.addElement(enteringEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs'));
} else {
enteringContentAnimation.addElement(contentEl);
enteringContentAnimation.addElement(headerEls);
}
rootAnimation.addAnimation(enteringContentAnimation);
if (backDirection) {
enteringContentAnimation
.beforeClearStyles([OPACITY])
.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`)
.fromTo(OPACITY, OFF_OPACITY, 1);
} else {
// entering content, forward direction
enteringContentAnimation
.beforeClearStyles([OPACITY])
.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
}
enteringToolBarEls.forEach((enteringToolBarEl, i) => {
const enteringToolBar = createAnimation(`ios-entering-toolbar-${i}`);
enteringToolBar.addElement(enteringToolBarEl);
rootAnimation.addAnimation(enteringToolBar);
const enteringTitle = createAnimation(`ios-entering-toolbar-${i}-title`);
enteringTitle.addElement(enteringToolBarEl.querySelector('ion-title'));
const enteringToolBarButtons = createAnimation(`ios-entering-toolbar-${i}-buttons`);
enteringToolBarButtons.addElement(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'));
const enteringToolBarItems = createAnimation(`ios-entering-toolbar-${i}-items`);
enteringToolBarItems.addElement(enteringToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])'));
const enteringToolBarBg = createAnimation(`ios-entering-toolbar-${i}-bg`);
enteringToolBarBg.addElement(shadow(enteringToolBarEl).querySelector('.toolbar-background'));
const enteringBackButton = createAnimation(`ios-entering-toolbar-${i}-back-button`);
const backButtonEl = enteringToolBarEl.querySelector('ion-back-button');
if (backButtonEl) {
enteringBackButton.addElement(backButtonEl);
}
enteringToolBar.addAnimation([enteringTitle, enteringToolBarButtons, enteringToolBarItems, enteringToolBarBg, enteringBackButton]);
enteringTitle.fromTo(OPACITY, 0.01, 1);
enteringToolBarButtons.fromTo(OPACITY, 0.01, 1);
enteringToolBarItems.fromTo(OPACITY, 0.01, 1);
if (backDirection) {
enteringTitle.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`);
enteringToolBarItems.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`);
// back direction, entering page has a back button
enteringBackButton.fromTo(OPACITY, 0.01, 1);
} else {
// entering toolbar, forward direction
enteringTitle.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
enteringToolBarItems.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
enteringToolBarBg
.beforeClearStyles([OPACITY])
.fromTo(OPACITY, 0.01, 1);
// forward direction, entering page has a back button
enteringBackButton.fromTo(OPACITY, 0.01, 1);
if (backButtonEl) {
const enteringBackBtnText = createAnimation(`ios-entering-toolbar-${i}-back-button-text`);
enteringBackBtnText
.addElement(shadow(backButtonEl).querySelector('.button-text'))
.fromTo(`transform`, (isRTL ? 'translateX(-100px)' : 'translateX(100px)'), 'translateX(0px)');
enteringToolBar.addAnimation(enteringBackBtnText);
}
}
});
// setup leaving view
if (leavingEl) {
const leavingContent = createAnimation(`ios-leaving-content-animation`);
leavingContent.addElement(leavingEl.querySelector(':scope > ion-content'));
leavingContent.addElement(leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *'));
rootAnimation.addAnimation(leavingContent);
if (backDirection) {
// leaving content, back direction
leavingContent
.beforeClearStyles([OPACITY])
.fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)'));
} else {
// leaving content, forward direction
leavingContent
.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
.fromTo(OPACITY, 1, OFF_OPACITY);
}
const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar');
leavingToolBarEls.forEach((leavingToolBarEl, i) => {
const leavingToolBar = createAnimation(`ios-leaving-toolbar-${i}-animation`);
leavingToolBar.addElement(leavingToolBarEl);
const leavingTitle = createAnimation(`ios-leaving-toolbar-${i}-title-animation`);
leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title'));
const leavingToolBarButtons = createAnimation(`ios-leaving-toolbar-${i}-buttons-animation`);
leavingToolBarButtons.addElement(leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'));
const leavingToolBarItems = createAnimation(`ios-leaving-toolbar-${i}-items-animation`);
const leavingToolBarItemEls = leavingToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])');
if (leavingToolBarItemEls.length > 0) {
leavingToolBarItems.addElement(leavingToolBarItemEls);
}
const leavingToolBarBg = createAnimation(`ios-leaving-toolbar-${i}-bg-animation`);
leavingToolBarBg.addElement(shadow(leavingToolBarEl).querySelector('.toolbar-background'));
const leavingBackButton = createAnimation(`ios-leaving-toolbar-${i}-back-button-animation`);
const backButtonEl = leavingToolBarEl.querySelector('ion-back-button');
if (backButtonEl) {
leavingBackButton.addElement(backButtonEl);
}
leavingToolBar.addAnimation([leavingTitle, leavingToolBarButtons, leavingToolBarItems, leavingBackButton, leavingToolBarBg]);
rootAnimation.addAnimation(leavingToolBar);
// fade out leaving toolbar items
leavingBackButton.fromTo(OPACITY, 0.99, 0);
leavingTitle.fromTo(OPACITY, 0.99, 0);
leavingToolBarButtons.fromTo(OPACITY, 0.99, 0);
leavingToolBarItems.fromTo(OPACITY, 0.99, 0);
if (backDirection) {
// leaving toolbar, back direction
leavingTitle.fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)'));
leavingToolBarItems.fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)'));
// leaving toolbar, back direction, and there's no entering toolbar
// should just slide out, no fading out
leavingToolBarBg
.beforeClearStyles([OPACITY])
.fromTo(OPACITY, 1, 0.01);
if (backButtonEl) {
const leavingBackBtnText = createAnimation('ios-leaving-toolbar-${i}-back-button-text');
leavingBackBtnText.addElement(shadow(backButtonEl).querySelector('.button-text'));
leavingBackBtnText.fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`);
leavingToolBar.addAnimation(leavingBackBtnText);
}
} else {
// leaving toolbar, forward direction
leavingTitle
.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
.afterClearStyles([TRANSFORM]);
leavingToolBarItems
.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
.afterClearStyles([TRANSFORM, OPACITY]);
leavingBackButton.afterClearStyles([OPACITY]);
leavingTitle.afterClearStyles([OPACITY]);
leavingToolBarButtons.afterClearStyles([OPACITY]);
}
});
}
return rootAnimation as any;
} catch (err) {
throw err;
}
};
export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement, opts: TransitionOptions): Promise<Animation> => {
const isRTL = (navEl.ownerDocument as any).dir === 'rtl';

View File

@@ -1,10 +1,65 @@
import { Animation } from '../../interface';
import { Animation as AnimationNew, createAnimation } from '../animation/animation';
import { TransitionOptions } from '../transition';
const TRANSLATEY = 'translateY';
const OFF_BOTTOM = '40px';
const CENTER = '0px';
export const newMdTransitionAnimation = (opts: TransitionOptions): AnimationNew => {
try {
const rootAnimation = createAnimation('md-root-animation');
const enteringEl = opts.enteringEl;
const leavingEl = opts.leavingEl;
const backDirection = (opts.direction === 'back');
const ionPageElement = getIonPageElement(enteringEl);
const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar');
rootAnimation
.addElement(ionPageElement)
.beforeRemoveClass('ion-page-invisible')
.fill('both');
if (backDirection) {
rootAnimation
.duration(opts.duration || 200)
.easing('cubic-bezier(0.47,0,0.745,0.715)');
} else {
rootAnimation
.duration(opts.duration || 280)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.fromTo('transform', `translateY(${OFF_BOTTOM})`, `translateY(${CENTER})`)
.fromTo('opacity', 0.01, 1);
}
if (enteringToolbarEle) {
const enteringToolBarAnimation = createAnimation('md-entering-toolbar-animation');
enteringToolBarAnimation.addElement(enteringToolbarEle);
rootAnimation.addAnimation(enteringToolBarAnimation);
}
if (leavingEl && backDirection) {
rootAnimation
.duration(opts.duration || 200)
.easing('cubic-bezier(0.47,0,0.745,0.715)');
const leavingPageAnimation = createAnimation('md-leaving-page-animation');
leavingPageAnimation
.addElement(getIonPageElement(leavingEl))
.fromTo('transform', `translateY(${CENTER})`, `translateY(${OFF_BOTTOM})`)
.fromTo('opacity', 1, 0);
rootAnimation.addAnimation(leavingPageAnimation);
}
return rootAnimation;
} catch (err) {
throw err;
}
};
export const mdTransitionAnimation = (AnimationC: Animation, _: HTMLElement, opts: TransitionOptions): Promise<Animation> => {
const enteringEl = opts.enteringEl;