UNPKG

@mui/material

Version:

Quickly build beautiful React apps. MUI is a simple and customizable component library to build faster, beautiful, and more accessible React applications. Follow your own design system, or start with Material Design.

345 lines (310 loc) 9.35 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; const _excluded = ["center", "classes", "className"]; let _ = t => t, _t, _t2, _t3, _t4; import * as React from 'react'; import PropTypes from 'prop-types'; import { TransitionGroup } from 'react-transition-group'; import clsx from 'clsx'; import { keyframes } from '@mui/system'; import styled from '../styles/styled'; import useThemeProps from '../styles/useThemeProps'; import Ripple from './Ripple'; import touchRippleClasses from './touchRippleClasses'; import { jsx as _jsx } from "react/jsx-runtime"; const DURATION = 550; export const DELAY_RIPPLE = 80; const enterKeyframe = keyframes(_t || (_t = _` 0% { transform: scale(0); opacity: 0.1; } 100% { transform: scale(1); opacity: 0.3; } `)); const exitKeyframe = keyframes(_t2 || (_t2 = _` 0% { opacity: 1; } 100% { opacity: 0; } `)); const pulsateKeyframe = keyframes(_t3 || (_t3 = _` 0% { transform: scale(1); } 50% { transform: scale(0.92); } 100% { transform: scale(1); } `)); export const TouchRippleRoot = styled('span', { name: 'MuiTouchRipple', slot: 'Root', skipSx: true })({ overflow: 'hidden', pointerEvents: 'none', position: 'absolute', zIndex: 0, top: 0, right: 0, bottom: 0, left: 0, borderRadius: 'inherit' }); // This `styled()` function invokes keyframes. `styled-components` only supports keyframes // in string templates. Do not convert these styles in JS object as it will break. export const TouchRippleRipple = styled(Ripple, { name: 'MuiTouchRipple', slot: 'Ripple' })(_t4 || (_t4 = _` opacity: 0; position: absolute; &.${0} { opacity: 0.3; transform: scale(1); animation-name: ${0}; animation-duration: ${0}ms; animation-timing-function: ${0}; } &.${0} { animation-duration: ${0}ms; } & .${0} { opacity: 1; display: block; width: 100%; height: 100%; border-radius: 50%; background-color: currentColor; } & .${0} { opacity: 0; animation-name: ${0}; animation-duration: ${0}ms; animation-timing-function: ${0}; } & .${0} { position: absolute; /* @noflip */ left: 0px; top: 0; animation-name: ${0}; animation-duration: 2500ms; animation-timing-function: ${0}; animation-iteration-count: infinite; animation-delay: 200ms; } `), touchRippleClasses.rippleVisible, enterKeyframe, DURATION, ({ theme }) => theme.transitions.easing.easeInOut, touchRippleClasses.ripplePulsate, ({ theme }) => theme.transitions.duration.shorter, touchRippleClasses.child, touchRippleClasses.childLeaving, exitKeyframe, DURATION, ({ theme }) => theme.transitions.easing.easeInOut, touchRippleClasses.childPulsate, pulsateKeyframe, ({ theme }) => theme.transitions.easing.easeInOut); /** * @ignore - internal component. * * TODO v5: Make private */ const TouchRipple = /*#__PURE__*/React.forwardRef(function TouchRipple(inProps, ref) { const props = useThemeProps({ props: inProps, name: 'MuiTouchRipple' }); const { center: centerProp = false, classes = {}, className } = props, other = _objectWithoutPropertiesLoose(props, _excluded); const [ripples, setRipples] = React.useState([]); const nextKey = React.useRef(0); const rippleCallback = React.useRef(null); React.useEffect(() => { if (rippleCallback.current) { rippleCallback.current(); rippleCallback.current = null; } }, [ripples]); // Used to filter out mouse emulated events on mobile. const ignoringMouseDown = React.useRef(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. const startTimer = React.useRef(null); // This is the hook called once the previous timeout is ready. const startTimerCommit = React.useRef(null); const container = React.useRef(null); React.useEffect(() => { return () => { clearTimeout(startTimer.current); }; }, []); const startCommit = React.useCallback(params => { const { pulsate, rippleX, rippleY, rippleSize, cb } = params; setRipples(oldRipples => [...oldRipples, /*#__PURE__*/_jsx(TouchRippleRipple, { classes: { ripple: clsx(classes.ripple, touchRippleClasses.ripple), rippleVisible: clsx(classes.rippleVisible, touchRippleClasses.rippleVisible), ripplePulsate: clsx(classes.ripplePulsate, touchRippleClasses.ripplePulsate), child: clsx(classes.child, touchRippleClasses.child), childLeaving: clsx(classes.childLeaving, touchRippleClasses.childLeaving), childPulsate: clsx(classes.childPulsate, touchRippleClasses.childPulsate) }, timeout: DURATION, pulsate: pulsate, rippleX: rippleX, rippleY: rippleY, rippleSize: rippleSize }, nextKey.current)]); nextKey.current += 1; rippleCallback.current = cb; }, [classes]); const start = React.useCallback((event = {}, options = {}, cb) => { const { pulsate = false, center = centerProp || options.pulsate, fakeElement = false // For test purposes } = options; if (event.type === 'mousedown' && ignoringMouseDown.current) { ignoringMouseDown.current = false; return; } if (event.type === 'touchstart') { ignoringMouseDown.current = true; } const element = fakeElement ? null : container.current; 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, clientY } = event.touches ? event.touches[0] : event; 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 is 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) { // check that this isn't another touchstart due to multitouch // otherwise we will only clear a single timer when unmounting while two // are running if (startTimerCommit.current === null) { // Prepare the ripple effect. startTimerCommit.current = () => { startCommit({ pulsate, rippleX, rippleY, rippleSize, cb }); }; // Delay the execution of the ripple effect. startTimer.current = setTimeout(() => { if (startTimerCommit.current) { startTimerCommit.current(); startTimerCommit.current = null; } }, DELAY_RIPPLE); // We have to make a tradeoff with this value. } } else { startCommit({ pulsate, rippleX, rippleY, rippleSize, cb }); } }, [centerProp, startCommit]); const pulsate = React.useCallback(() => { start({}, { pulsate: true }); }, [start]); const stop = React.useCallback((event, cb) => { clearTimeout(startTimer.current); // The touch interaction occurs too quickly. // We still want to show ripple effect. if (event.type === 'touchend' && startTimerCommit.current) { startTimerCommit.current(); startTimerCommit.current = null; startTimer.current = setTimeout(() => { stop(event, cb); }); return; } startTimerCommit.current = null; setRipples(oldRipples => { if (oldRipples.length > 0) { return oldRipples.slice(1); } return oldRipples; }); rippleCallback.current = cb; }, []); React.useImperativeHandle(ref, () => ({ pulsate, start, stop }), [pulsate, start, stop]); return /*#__PURE__*/_jsx(TouchRippleRoot, _extends({ className: clsx(classes.root, touchRippleClasses.root, className), ref: container }, other, { children: /*#__PURE__*/_jsx(TransitionGroup, { component: null, exit: true, children: ripples }) })); }); process.env.NODE_ENV !== "production" ? TouchRipple.propTypes = { /** * 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) below for more details. */ classes: PropTypes.object, /** * @ignore */ className: PropTypes.string } : void 0; export default TouchRipple;