Skip to content
This repository was archived by the owner on Apr 8, 2020. It is now read-only.

Commit 3f41255

Browse files
committed
Prevent Phantom Events
1 parent b7dba0d commit 3f41255

7 files changed

+151
-1
lines changed

mocha.opts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
--compilers tsx:ts-node/register,ts:ts-node/register
22
--reporter dot
3+
test/jsdom.ts
34
src/**/*.spec.tsx
45
src/**/*.spec.ts

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"devDependencies": {
5252
"@types/chai": "^3.4.34",
5353
"@types/enzyme": "^2.5.37",
54+
"@types/jsdom": "^2.0.29",
5455
"@types/mocha": "^2.2.32",
5556
"@types/react": "^0.14.43",
5657
"@types/react-addons-transition-group": "^0.14.17",
@@ -73,6 +74,7 @@
7374
"gulp-yaml-validate": "^1.0.2",
7475
"istanbul": "^1.1.0-alpha.1",
7576
"istanbul-instrumenter-loader": "^1.1.0",
77+
"jsdom": "^9.9.1",
7678
"json-loader": "^0.5.4",
7779
"karma": "^1.3.0",
7880
"karma-chrome-launcher": "^2.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @license
3+
* Copyright (C) 2016-present Chi Vinh Le and contributors.
4+
*
5+
* This software may be modified and distributed under the terms
6+
* of the MIT license. See the LICENSE file for details.
7+
*/
8+
9+
import * as React from "react";
10+
import { StatelessComponent } from "react";
11+
import { assert } from "chai";
12+
import { mount, ReactWrapper } from "enzyme";
13+
import { spy, SinonSpy } from "sinon";
14+
import { assemble } from "react-assemble";
15+
16+
import { preventPhantomEvents } from "./preventPhantomEvents";
17+
const Component: StatelessComponent<any> = ({onTransitionEnd}) => <span {...{ onTransitionEnd }} />;
18+
19+
describe("preventPhantomEvents", () => {
20+
const composable = preventPhantomEvents;
21+
const Assembly = assemble<any, any>(composable)(Component);
22+
let onTransitionEnd: SinonSpy;
23+
let wrapper: ReactWrapper<any, any>;
24+
25+
beforeEach(() => {
26+
onTransitionEnd = spy();
27+
wrapper = mount(<Assembly onTransitionEnd={onTransitionEnd} />);
28+
});
29+
30+
it("should block onTransitionEnd", () => {
31+
wrapper.setProps({ active: true });
32+
const event = { timeStamp: Date.now() };
33+
wrapper.simulate("transitionEnd", event);
34+
assert.isTrue(onTransitionEnd.notCalled);
35+
});
36+
37+
it("should call onTransitionEnd", () => {
38+
wrapper.setProps({ active: true });
39+
const event = { timeStamp: Date.now() + 20 };
40+
wrapper.simulate("transitionEnd", event);
41+
assert.isTrue(onTransitionEnd.called);
42+
});
43+
44+
it("should block onTransitionEnd comparing precision time", () => {
45+
wrapper.setProps({ active: true });
46+
const event = { timeStamp: performance.now() };
47+
wrapper.simulate("transitionEnd", event);
48+
assert.isTrue(onTransitionEnd.notCalled);
49+
});
50+
51+
it("should call onTransitionEnd comparing precision time", () => {
52+
wrapper.setProps({ active: true });
53+
const event = { timeStamp: performance.now() + 20 };
54+
wrapper.simulate("transitionEnd", event);
55+
assert.isTrue(onTransitionEnd.called);
56+
});
57+
});
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @license
3+
* Copyright (C) 2016-present Chi Vinh Le and contributors.
4+
*
5+
* This software may be modified and distributed under the terms
6+
* of the MIT license. See the LICENSE file for details.
7+
*/
8+
9+
import { TransitionEvent } from "react";
10+
import { combine, withHandlers, onWillReceiveProps, onDidUpdate, isolate, integrate } from "react-assemble";
11+
12+
export const preventPhantomEvents = combine(
13+
isolate(
14+
withHandlers<any, any>(() => {
15+
let lastTriggerTime: any;
16+
let lastTriggerTimePerformance: any;
17+
let timeUpdateRequested = false;
18+
return {
19+
requestTimeUpdate: () => () => {
20+
timeUpdateRequested = true;
21+
},
22+
handleTimeUpdateRequest: () => () => {
23+
if (timeUpdateRequested) {
24+
lastTriggerTime = Date.now();
25+
if (typeof performance !== "undefined") {
26+
lastTriggerTimePerformance = performance.now();
27+
}
28+
timeUpdateRequested = false;
29+
}
30+
},
31+
onTransitionEnd: ({onTransitionEnd}: any) => (e: TransitionEvent) => {
32+
if (!onTransitionEnd) { return; }
33+
34+
// Skip transitionEnd that comes <= 15ms after (reversing) a transition.
35+
// In most cases this came from the previous transition.
36+
let compareWith = lastTriggerTime;
37+
if ((e.timeStamp as any) < 1000000000000 && lastTriggerTimePerformance) {
38+
compareWith = lastTriggerTimePerformance;
39+
}
40+
if ((e.timeStamp as any) - compareWith <= 15) {
41+
return;
42+
}
43+
44+
onTransitionEnd(e);
45+
},
46+
};
47+
}),
48+
onWillReceiveProps<any>(({active}, {active: nextActive, requestTimeUpdate}) => {
49+
if (active !== nextActive) {
50+
requestTimeUpdate();
51+
}
52+
}),
53+
onDidUpdate<any>(({handleTimeUpdateRequest}) => handleTimeUpdateRequest()),
54+
integrate<any>("onTransitionEnd"),
55+
),
56+
);

src/csstransition.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { mergeWithStyle } from "./composables/mergeWithStyle";
1616
import { withTransitionInfo } from "./composables/withTransitionInfo";
1717
import { withTransitionObserver } from "./composables/withTransitionObserver";
1818
import { withWorkaround } from "./composables/withWorkaround";
19+
import { preventPhantomEvents } from "./composables/preventPhantomEvents";
1920

2021
export type CSSTransitionDelay = number | { appear?: number; enter?: number; leave?: number };
2122
export type CSSTransitionEventHandler = () => void;
@@ -76,6 +77,7 @@ const enhance = assemble<CSSTransitionInnerProps, CSSTransitionProps>(
7677
withTransitionInfo,
7778
withTransitionObserver,
7879
withWorkaround,
80+
preventPhantomEvents,
7981
mapPropsToInner,
8082
);
8183

test/jsdom.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @license
3+
* Copyright (C) 2016-2017 Chi Vinh Le and contributors.
4+
*
5+
* This software may be modified and distributed under the terms
6+
* of the MIT license. See the LICENSE file for details.
7+
*/
8+
9+
import { jsdom } from "jsdom";
10+
11+
declare var global: any;
12+
13+
function clock(start?: any): any {
14+
if (!start) { return process.hrtime(); }
15+
const end = process.hrtime(start);
16+
return Math.round((end[0] * 1000) + (end[1] / 1000000));
17+
}
18+
19+
const start = clock();
20+
global.document = jsdom("");
21+
global.window = document.defaultView;
22+
global.navigator = { userAgent: "node.js" };
23+
// Setup a fake version of performance.now.
24+
global.performance = { now: () => clock(start) };
25+
26+
const exposedProperties = ["document", "window", "navigator"];
27+
Object.keys(window).forEach((property) => {
28+
if (typeof global[property] === "undefined") {
29+
exposedProperties.push(property);
30+
global[property] = (window as any)[property];
31+
}
32+
});

tests.webpack.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
const srcContext = require.context("./src", true, /^(?!.*\.(spec|d)\.).*\.tsx?$/);
44
srcContext.keys().forEach(srcContext);
55

6-
const testContext = require.context("./test", true, /\.tsx?$/);
6+
const testContext = require.context("./test", true, /\.test\.tsx?$/);
77
testContext.keys().forEach(testContext);

0 commit comments

Comments
 (0)