Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
"size-limit": [
{
"path": "build/index.js",
"limit": "92 KB"
"limit": "93 KB"
}
],
"nyc": {
Expand Down
8 changes: 8 additions & 0 deletions src/ButtonBase/ButtonBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export type Props = {
* @ignore
*/
onTouchEnd?: Function,
/**
* @ignore
*/
onTouchMove?: Function,
/**
* @ignore
*/
Expand Down Expand Up @@ -250,6 +254,8 @@ class ButtonBase extends React.Component<ProvidedProps & Props, State> {

handleTouchEnd = createRippleHandler(this, 'TouchEnd', 'stop');

handleTouchMove = createRippleHandler(this, 'TouchEnd', 'stop');

handleBlur = createRippleHandler(this, 'Blur', 'stop', () => {
this.setState({ keyboardFocused: false });
});
Expand Down Expand Up @@ -315,6 +321,7 @@ class ButtonBase extends React.Component<ProvidedProps & Props, State> {
onMouseLeave,
onMouseUp,
onTouchEnd,
onTouchMove,
onTouchStart,
rootRef,
tabIndex,
Expand Down Expand Up @@ -362,6 +369,7 @@ class ButtonBase extends React.Component<ProvidedProps & Props, State> {
onMouseLeave={this.handleMouseLeave}
onMouseUp={this.handleMouseUp}
onTouchEnd={this.handleTouchEnd}
onTouchMove={this.handleTouchMove}
onTouchStart={this.handleTouchStart}
ref={rootRef}
tabIndex={disabled ? -1 : tabIndex}
Expand Down
48 changes: 46 additions & 2 deletions src/ButtonBase/TouchRipple.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import withStyles from '../styles/withStyles';
import Ripple from './Ripple';

const DURATION = 550;
export const DELAY_RIPPLE = 80;

export const styles = (theme: Object) => ({
root: {
Expand Down Expand Up @@ -123,8 +124,17 @@ class TouchRipple extends React.Component<ProvidedProps & Props, State> {
ripples: [],
};

componentWillUnmount() {
clearTimeout(this.startTimer);
}

// Used to filter out mouse emulated events on mobile.
ignoringMouseDown = false;
// We use a timer in order to only show the ripples for touch "click" like events.
// We don't want to display the ripple for touch scroll events.
startTimer = null;
// This is the hook called once the previous timeout is ready.
startTimerCommit = null;

pulsate = () => {
this.start({}, { pulsate: true });
Expand All @@ -142,8 +152,6 @@ class TouchRipple extends React.Component<ProvidedProps & Props, State> {
this.ignoringMouseDown = true;
}

let ripples = this.state.ripples;

const element = ReactDOM.findDOMNode(this);
const rect = element
? // $FlowFixMe
Expand Down Expand Up @@ -201,6 +209,26 @@ class TouchRipple extends React.Component<ProvidedProps & Props, State> {
rippleSize = Math.sqrt(Math.pow(sizeX, 2) + Math.pow(sizeY, 2));
}

// Touche devices
if (event.touches) {
// Prepare the ripple effect.
this.startTimerCommit = () => {
this.startCommit({ pulsate, rippleX, rippleY, rippleSize, cb });
};
// Deplay the execution of the ripple effect.
this.startTimer = setTimeout(() => {
this.startTimerCommit();
this.startTimerCommit = null;
}, DELAY_RIPPLE); // We have to make a tradeoff with this value.
} else {
this.startCommit({ pulsate, rippleX, rippleY, rippleSize, cb });
}
};

startCommit = params => {
const { pulsate, rippleX, rippleY, rippleSize, cb } = params;
let ripples = this.state.ripples;

// Add a ripple to the ripples array
ripples = [
...ripples,
Expand Down Expand Up @@ -228,7 +256,23 @@ class TouchRipple extends React.Component<ProvidedProps & Props, State> {
};

stop = (event, cb) => {
clearTimeout(this.startTimer);
const { ripples } = this.state;

// The touch interaction occures to quickly.
// We still want to show ripple effect.
if (event.type === 'touchend' && this.startTimerCommit) {
event.persist();
this.startTimerCommit();
this.startTimerCommit = null;
this.startTimer = setTimeout(() => {
this.stop(event, cb);
}, 0);
return;
}

this.startTimerCommit = null;

if (ripples && ripples.length) {
this.setState(
{
Expand Down
133 changes: 86 additions & 47 deletions src/ButtonBase/TouchRipple.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// @flow

import React from 'react';
import { useFakeTimers } from 'sinon';
import { assert } from 'chai';
import { createShallow, getClasses } from '../test-utils';
import TouchRipple from './TouchRipple';
import TouchRipple, { DELAY_RIPPLE } from './TouchRipple';

describe('<TouchRipple />', () => {
let shallow;
Expand All @@ -18,11 +19,7 @@ describe('<TouchRipple />', () => {
const wrapper = shallow(<TouchRipple />);
assert.strictEqual(wrapper.name(), 'TransitionGroup');
assert.strictEqual(wrapper.props().component, 'span', 'should be pass a span as the component');
});

it('should have the root class', () => {
const wrapper = shallow(<TouchRipple />);
assert.strictEqual(wrapper.hasClass(classes.root), true);
assert.strictEqual(wrapper.hasClass(classes.root), true, 'should have the root class');
});

it('should render the custom className', () => {
Expand All @@ -40,72 +37,53 @@ describe('<TouchRipple />', () => {
});
});

describe('creating individual ripples', () => {
let wrapper;
let instance;

before(() => {
wrapper = shallow(<TouchRipple />);
instance = wrapper.instance();
});
it('should create individual ripples', () => {
const wrapper = shallow(<TouchRipple />);
const instance = wrapper.instance();

it('should start with no ripples', () => {
assert.strictEqual(wrapper.state('ripples').length, 0);
});
assert.strictEqual(wrapper.state().ripples.length, 0, 'should start with no ripples');

it('should create a ripple', () => {
instance.start({ clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state('ripples').length, 1);
});
instance.start({ clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state().ripples.length, 1, 'should create a ripple');

it('should create another ripple', () => {
instance.start({ clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state('ripples').length, 2);
});
instance.start({ clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state().ripples.length, 2, 'should create another ripple');

it('should create another ripple', () => {
instance.start({ clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state('ripples').length, 3);
});
instance.start({ clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state().ripples.length, 3, 'should create another ripple');

it('should remove a ripple', () => {
instance.stop();
assert.strictEqual(wrapper.state('ripples').length, 2);
});
instance.stop({ type: 'mouseup' });
assert.strictEqual(wrapper.state().ripples.length, 2, 'should remove a ripple');

it('should remove a ripple', () => {
instance.stop();
assert.strictEqual(wrapper.state('ripples').length, 1);
});
instance.stop({ type: 'mouseup' });
assert.strictEqual(wrapper.state().ripples.length, 1, 'should remove a ripple');

it('should remove another ripple', () => {
instance.stop();
assert.strictEqual(wrapper.state('ripples').length, 0);
});
instance.stop({ type: 'mouseup' });
assert.strictEqual(wrapper.state().ripples.length, 0, 'should remove all the ripples');
});

describe('creating unique ripples', () => {
it('should create a ripple', () => {
const wrapper = shallow(<TouchRipple />);
const instance = wrapper.instance();
instance.pulsate();
assert.strictEqual(wrapper.state('ripples').length, 1);
assert.strictEqual(wrapper.state().ripples.length, 1);
});

it('should ignore a mousedown event', () => {
const wrapper = shallow(<TouchRipple />);
const instance = wrapper.instance();
instance.ignoringMouseDown = true;
instance.start({ type: 'mousedown' });
assert.strictEqual(wrapper.state('ripples').length, 0);
assert.strictEqual(wrapper.state().ripples.length, 0);
});

it('should set ignoringMouseDown to true', () => {
const wrapper = shallow(<TouchRipple />);
const instance = wrapper.instance();
assert.strictEqual(instance.ignoringMouseDown, false);
instance.start({ type: 'touchstart' });
assert.strictEqual(wrapper.state('ripples').length, 1);
assert.strictEqual(wrapper.state().ripples.length, 1);
assert.strictEqual(instance.ignoringMouseDown, true);
});

Expand All @@ -115,9 +93,70 @@ describe('<TouchRipple />', () => {
const clientX = 1;
const clientY = 1;
instance.start({ clientX, clientY });
assert.strictEqual(wrapper.state('ripples').length, 1);
assert.strictEqual(wrapper.state('ripples')[0].props.rippleX, clientX);
assert.strictEqual(wrapper.state('ripples')[0].props.rippleY, clientY);
assert.strictEqual(wrapper.state().ripples.length, 1);
assert.strictEqual(wrapper.state().ripples[0].props.rippleX, clientX);
assert.strictEqual(wrapper.state().ripples[0].props.rippleY, clientY);
});
});

describe('mobile', () => {
let clock;

before(() => {
clock = useFakeTimers();
});

after(() => {
clock.restore();
});

it('should delay the display of the ripples', () => {
const wrapper = shallow(<TouchRipple />);
const instance = wrapper.instance();

assert.strictEqual(wrapper.state().ripples.length, 0);
instance.start({ touches: [], clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state().ripples.length, 0);

clock.tick(DELAY_RIPPLE);
assert.strictEqual(wrapper.state().ripples.length, 1);

clock.tick(DELAY_RIPPLE);
instance.stop({ type: 'touchend' });
assert.strictEqual(wrapper.state().ripples.length, 0);
});

it('should trigger the ripple for short touch interactions', () => {
const wrapper = shallow(<TouchRipple />);
const instance = wrapper.instance();

assert.strictEqual(wrapper.state().ripples.length, 0);
instance.start({ touches: [], clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state().ripples.length, 0);

clock.tick(DELAY_RIPPLE / 2);
assert.strictEqual(wrapper.state().ripples.length, 0);
instance.stop({ type: 'touchend', persist: () => {} });
assert.strictEqual(wrapper.state().ripples.length, 1);

clock.tick(1);
assert.strictEqual(wrapper.state().ripples.length, 0);
});

it('should interupt the ripple schedule', () => {
const wrapper = shallow(<TouchRipple />);
const instance = wrapper.instance();

assert.strictEqual(wrapper.state().ripples.length, 0);
instance.start({ touches: [], clientX: 0, clientY: 0 });
assert.strictEqual(wrapper.state().ripples.length, 0);

clock.tick(DELAY_RIPPLE / 2);
assert.strictEqual(wrapper.state().ripples.length, 0);

instance.stop({ type: 'touchmove' });
clock.tick(DELAY_RIPPLE);
assert.strictEqual(wrapper.state().ripples.length, 0);
});
});
});