UNPKG

@material-ui/core

Version:

React components that implement Google's Material Design.

301 lines (268 loc) 7.76 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import TransitionGroup from 'react-transition-group/TransitionGroup'; import classNames from 'classnames'; import withStyles from '../styles/withStyles'; import Ripple from './Ripple'; const DURATION = 550; export const DELAY_RIPPLE = 80; export const styles = theme => ({ /* Styles applied to the root element. */ root: { display: 'block', position: 'absolute', overflow: 'hidden', borderRadius: 'inherit', width: '100%', height: '100%', left: 0, top: 0, pointerEvents: 'none', zIndex: 0 }, /* Styles applied to the internal `Ripple` components `ripple` class. */ ripple: { width: 50, height: 50, left: 0, top: 0, opacity: 0, position: 'absolute' }, /* Styles applied to the internal `Ripple` components `rippleVisible` class. */ rippleVisible: { opacity: 0.3, transform: 'scale(1)', animation: `mui-ripple-enter ${DURATION}ms ${theme.transitions.easing.easeInOut}` }, /* Styles applied to the internal `Ripple` components `ripplePulsate` class. */ ripplePulsate: { animationDuration: `${theme.transitions.duration.shorter}ms` }, /* Styles applied to the internal `Ripple` components `child` class. */ child: { opacity: 1, display: 'block', width: '100%', height: '100%', borderRadius: '50%', backgroundColor: 'currentColor' }, /* Styles applied to the internal `Ripple` components `childLeaving` class. */ childLeaving: { opacity: 0, animation: `mui-ripple-exit ${DURATION}ms ${theme.transitions.easing.easeInOut}` }, /* Styles applied to the internal `Ripple` components `childPulsate` class. */ childPulsate: { position: 'absolute', left: 0, top: 0, animation: `mui-ripple-pulsate 2500ms ${theme.transitions.easing.easeInOut} 200ms infinite` }, '@keyframes mui-ripple-enter': { '0%': { transform: 'scale(0)', opacity: 0.1 }, '100%': { transform: 'scale(1)', opacity: 0.3 } }, '@keyframes mui-ripple-exit': { '0%': { opacity: 1 }, '100%': { opacity: 0 } }, '@keyframes mui-ripple-pulsate': { '0%': { transform: 'scale(1)' }, '50%': { transform: 'scale(0.92)' }, '100%': { transform: 'scale(1)' } } }); class TouchRipple extends React.PureComponent { constructor(...args) { super(...args); this.state = { // eslint-disable-next-line react/no-unused-state nextKey: 0, ripples: [] }; this.pulsate = () => { this.start({}, { pulsate: true }); }; this.start = (event = {}, options = {}, cb) => { const { pulsate = false, center = this.props.center || options.pulsate, fakeElement = false // For test purposes } = options; if (event.type === 'mousedown' && this.ignoringMouseDown) { this.ignoringMouseDown = false; return; } if (event.type === 'touchstart') { this.ignoringMouseDown = true; } const element = fakeElement ? null : ReactDOM.findDOMNode(this); const rect = element ? element.getBoundingClientRect() : { width: 0, height: 0, left: 0, top: 0 }; // Get the size of the ripple let rippleX; let rippleY; let rippleSize; if (center || event.clientX === 0 && event.clientY === 0 || !event.clientX && !event.touches) { rippleX = Math.round(rect.width / 2); rippleY = Math.round(rect.height / 2); } else { const clientX = event.clientX ? event.clientX : event.touches[0].clientX; const clientY = event.clientY ? event.clientY : event.touches[0].clientY; rippleX = Math.round(clientX - rect.left); rippleY = Math.round(clientY - rect.top); } if (center) { rippleSize = Math.sqrt((2 * rect.width ** 2 + rect.height ** 2) / 3); // For some reason the animation is broken on Mobile Chrome if the size if even. if (rippleSize % 2 === 0) { rippleSize += 1; } } else { const sizeX = Math.max(Math.abs((element ? element.clientWidth : 0) - rippleX), rippleX) * 2 + 2; const sizeY = Math.max(Math.abs((element ? element.clientHeight : 0) - rippleY), rippleY) * 2 + 2; rippleSize = Math.sqrt(sizeX ** 2 + 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(() => { if (this.startTimerCommit) { this.startTimerCommit(); this.startTimerCommit = null; } }, DELAY_RIPPLE); // We have to make a tradeoff with this value. } else { this.startCommit({ pulsate, rippleX, rippleY, rippleSize, cb }); } }; this.startCommit = params => { const { pulsate, rippleX, rippleY, rippleSize, cb } = params; this.setState(state => { return { nextKey: state.nextKey + 1, ripples: [...state.ripples, React.createElement(Ripple, { key: state.nextKey, classes: this.props.classes, timeout: { exit: DURATION, enter: DURATION }, pulsate: pulsate, rippleX: rippleX, rippleY: rippleY, rippleSize: rippleSize })] }; }, cb); }; this.stop = (event, cb) => { clearTimeout(this.startTimer); const { ripples } = this.state; // The touch interaction occurs too 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({ ripples: ripples.slice(1) }, cb); } }; } componentWillUnmount() { clearTimeout(this.startTimer); } render() { const _this$props = this.props, { center, classes, className } = _this$props, other = _objectWithoutProperties(_this$props, ["center", "classes", "className"]); return React.createElement(TransitionGroup, _extends({ component: "span", enter: true, exit: true, className: classNames(classes.root, className) }, other), this.state.ripples); } } TouchRipple.propTypes = process.env.NODE_ENV !== "production" ? { /** * If `true`, the ripple starts at the center of the component * rather than at the point of interaction. */ center: PropTypes.bool, /** * Override or extend the styles applied to the component. * See [CSS API](#css-api) below for more details. */ classes: PropTypes.object.isRequired, /** * @ignore */ className: PropTypes.string } : {}; TouchRipple.defaultProps = { center: false }; export default withStyles(styles, { flip: false, name: 'MuiTouchRipple' })(TouchRipple);