material-ui
Version:
React Components that Implement Google's Material Design.
311 lines (253 loc) • 10.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');
var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _toArray2 = require('babel-runtime/helpers/toArray');
var _toArray3 = _interopRequireDefault(_toArray2);
var _simpleAssign = require('simple-assign');
var _simpleAssign2 = _interopRequireDefault(_simpleAssign);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _TransitionGroup = require('react-transition-group/TransitionGroup');
var _TransitionGroup2 = _interopRequireDefault(_TransitionGroup);
var _dom = require('../utils/dom');
var _dom2 = _interopRequireDefault(_dom);
var _CircleRipple = require('./CircleRipple');
var _CircleRipple2 = _interopRequireDefault(_CircleRipple);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Remove the first element of the array
var shift = function shift(_ref) {
var _ref2 = (0, _toArray3.default)(_ref),
newArray = _ref2.slice(1);
return newArray;
};
var TouchRipple = function (_Component) {
(0, _inherits3.default)(TouchRipple, _Component);
function TouchRipple(props, context) {
(0, _classCallCheck3.default)(this, TouchRipple);
// Touch start produces a mouse down event for compat reasons. To avoid
// showing ripples twice we skip showing a ripple for the first mouse down
// after a touch start. Note we don't store ignoreNextMouseDown in this.state
// to avoid re-rendering when we change it.
var _this = (0, _possibleConstructorReturn3.default)(this, (TouchRipple.__proto__ || (0, _getPrototypeOf2.default)(TouchRipple)).call(this, props, context));
_this.handleMouseDown = function (event) {
// only listen to left clicks
if (event.button === 0) {
_this.start(event, false);
}
};
_this.handleMouseUp = function () {
_this.end();
};
_this.handleMouseLeave = function () {
_this.end();
};
_this.handleTouchStart = function (event) {
event.stopPropagation();
// If the user is swiping (not just tapping), save the position so we can
// abort ripples if the user appears to be scrolling.
if (_this.props.abortOnScroll && event.touches) {
_this.startListeningForScrollAbort(event);
_this.startTime = Date.now();
}
_this.start(event, true);
};
_this.handleTouchEnd = function () {
_this.end();
};
_this.handleTouchMove = function (event) {
// Stop trying to abort if we're already 300ms into the animation
var timeSinceStart = Math.abs(Date.now() - _this.startTime);
if (timeSinceStart > 300) {
_this.stopListeningForScrollAbort();
return;
}
// If the user is scrolling...
var deltaY = Math.abs(event.touches[0].clientY - _this.firstTouchY);
var deltaX = Math.abs(event.touches[0].clientX - _this.firstTouchX);
// Call it a scroll after an arbitrary 6px (feels reasonable in testing)
if (deltaY > 6 || deltaX > 6) {
var currentRipples = _this.state.ripples;
var ripple = currentRipples[0];
// This clone will replace the ripple in ReactTransitionGroup with a
// version that will disappear immediately when removed from the DOM
var abortedRipple = _react2.default.cloneElement(ripple, { aborted: true });
// Remove the old ripple and replace it with the new updated one
currentRipples = shift(currentRipples);
currentRipples = [].concat((0, _toConsumableArray3.default)(currentRipples), [abortedRipple]);
_this.setState({ ripples: currentRipples }, function () {
// Call end after we've set the ripple to abort otherwise the setState
// in end() merges with this and the ripple abort fails
_this.end();
});
}
};
_this.ignoreNextMouseDown = false;
_this.state = {
// This prop allows us to only render the ReactTransitionGroup
// on the first click of the component, making the inital render faster.
hasRipples: false,
nextKey: 0,
ripples: []
};
return _this;
}
(0, _createClass3.default)(TouchRipple, [{
key: 'start',
value: function start(event, isRippleTouchGenerated) {
var theme = this.context.muiTheme.ripple;
if (this.ignoreNextMouseDown && !isRippleTouchGenerated) {
this.ignoreNextMouseDown = false;
return;
}
var ripples = this.state.ripples;
// Add a ripple to the ripples array
ripples = [].concat((0, _toConsumableArray3.default)(ripples), [_react2.default.createElement(_CircleRipple2.default, {
key: this.state.nextKey,
style: !this.props.centerRipple ? this.getRippleStyle(event) : {},
color: this.props.color || theme.color,
opacity: this.props.opacity,
touchGenerated: isRippleTouchGenerated
})]);
this.ignoreNextMouseDown = isRippleTouchGenerated;
this.setState({
hasRipples: true,
nextKey: this.state.nextKey + 1,
ripples: ripples
});
}
}, {
key: 'end',
value: function end() {
var currentRipples = this.state.ripples;
this.setState({
ripples: shift(currentRipples)
});
if (this.props.abortOnScroll) {
this.stopListeningForScrollAbort();
}
}
// Check if the user seems to be scrolling and abort the animation if so
}, {
key: 'startListeningForScrollAbort',
value: function startListeningForScrollAbort(event) {
this.firstTouchY = event.touches[0].clientY;
this.firstTouchX = event.touches[0].clientX;
// Note that when scolling Chrome throttles this event to every 200ms
// Also note we don't listen for scroll events directly as there's no general
// way to cover cases like scrolling within containers on the page
document.body.addEventListener('touchmove', this.handleTouchMove);
}
}, {
key: 'stopListeningForScrollAbort',
value: function stopListeningForScrollAbort() {
document.body.removeEventListener('touchmove', this.handleTouchMove);
}
}, {
key: 'getRippleStyle',
value: function getRippleStyle(event) {
var el = _reactDom2.default.findDOMNode(this);
var elHeight = el.offsetHeight;
var elWidth = el.offsetWidth;
var offset = _dom2.default.offset(el);
var isTouchEvent = event.touches && event.touches.length;
var pageX = isTouchEvent ? event.touches[0].pageX : event.pageX;
var pageY = isTouchEvent ? event.touches[0].pageY : event.pageY;
var pointerX = pageX - offset.left;
var pointerY = pageY - offset.top;
var topLeftDiag = this.calcDiag(pointerX, pointerY);
var topRightDiag = this.calcDiag(elWidth - pointerX, pointerY);
var botRightDiag = this.calcDiag(elWidth - pointerX, elHeight - pointerY);
var botLeftDiag = this.calcDiag(pointerX, elHeight - pointerY);
var rippleRadius = Math.max(topLeftDiag, topRightDiag, botRightDiag, botLeftDiag);
var rippleSize = rippleRadius * 2;
var left = pointerX - rippleRadius;
var top = pointerY - rippleRadius;
return {
directionInvariant: true,
height: rippleSize,
width: rippleSize,
top: top,
left: left
};
}
}, {
key: 'calcDiag',
value: function calcDiag(a, b) {
return Math.sqrt(a * a + b * b);
}
}, {
key: 'render',
value: function render() {
var _props = this.props,
children = _props.children,
style = _props.style;
var _state = this.state,
hasRipples = _state.hasRipples,
ripples = _state.ripples;
var prepareStyles = this.context.muiTheme.prepareStyles;
var rippleGroup = void 0;
if (hasRipples) {
var mergedStyles = (0, _simpleAssign2.default)({
height: '100%',
width: '100%',
position: 'absolute',
top: 0,
left: 0,
overflow: 'hidden',
pointerEvents: 'none',
zIndex: 1 // This is also needed so that ripples do not bleed past a parent border radius.
}, style);
rippleGroup = _react2.default.createElement(
_TransitionGroup2.default,
{ style: prepareStyles(mergedStyles) },
ripples
);
}
return _react2.default.createElement(
'div',
{
onMouseUp: this.handleMouseUp,
onMouseDown: this.handleMouseDown,
onMouseLeave: this.handleMouseLeave,
onTouchStart: this.handleTouchStart,
onTouchEnd: this.handleTouchEnd
},
rippleGroup,
children
);
}
}]);
return TouchRipple;
}(_react.Component);
TouchRipple.defaultProps = {
abortOnScroll: true
};
TouchRipple.contextTypes = {
muiTheme: _propTypes2.default.object.isRequired
};
TouchRipple.propTypes = process.env.NODE_ENV !== "production" ? {
abortOnScroll: _propTypes2.default.bool,
centerRipple: _propTypes2.default.bool,
children: _propTypes2.default.node,
color: _propTypes2.default.string,
opacity: _propTypes2.default.number,
style: _propTypes2.default.object
} : {};
exports.default = TouchRipple;