UNPKG

9.22 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
3const _excluded = ["center", "classes", "className"];
4import * as React from 'react';
5import PropTypes from 'prop-types';
6import { TransitionGroup } from 'react-transition-group';
7import clsx from 'clsx';
8import { keyframes } from '@mui/system';
9import styled from '../styles/styled';
10import useThemeProps from '../styles/useThemeProps';
11import Ripple from './Ripple';
12import touchRippleClasses from './touchRippleClasses';
13import { jsx as _jsx } from "react/jsx-runtime";
14const DURATION = 550;
15export const DELAY_RIPPLE = 80;
16const enterKeyframe = keyframes`
17 0% {
18 transform: scale(0);
19 opacity: 0.1;
20 }
21
22 100% {
23 transform: scale(1);
24 opacity: 0.3;
25 }
26`;
27const exitKeyframe = keyframes`
28 0% {
29 opacity: 1;
30 }
31
32 100% {
33 opacity: 0;
34 }
35`;
36const pulsateKeyframe = keyframes`
37 0% {
38 transform: scale(1);
39 }
40
41 50% {
42 transform: scale(0.92);
43 }
44
45 100% {
46 transform: scale(1);
47 }
48`;
49export const TouchRippleRoot = styled('span', {
50 name: 'MuiTouchRipple',
51 slot: 'Root'
52})({
53 overflow: 'hidden',
54 pointerEvents: 'none',
55 position: 'absolute',
56 zIndex: 0,
57 top: 0,
58 right: 0,
59 bottom: 0,
60 left: 0,
61 borderRadius: 'inherit'
62}); // This `styled()` function invokes keyframes. `styled-components` only supports keyframes
63// in string templates. Do not convert these styles in JS object as it will break.
64
65export const TouchRippleRipple = styled(Ripple, {
66 name: 'MuiTouchRipple',
67 slot: 'Ripple'
68})`
69 opacity: 0;
70 position: absolute;
71
72 &.${touchRippleClasses.rippleVisible} {
73 opacity: 0.3;
74 transform: scale(1);
75 animation-name: ${enterKeyframe};
76 animation-duration: ${DURATION}ms;
77 animation-timing-function: ${({
78 theme
79}) => theme.transitions.easing.easeInOut};
80 }
81
82 &.${touchRippleClasses.ripplePulsate} {
83 animation-duration: ${({
84 theme
85}) => theme.transitions.duration.shorter}ms;
86 }
87
88 & .${touchRippleClasses.child} {
89 opacity: 1;
90 display: block;
91 width: 100%;
92 height: 100%;
93 border-radius: 50%;
94 background-color: currentColor;
95 }
96
97 & .${touchRippleClasses.childLeaving} {
98 opacity: 0;
99 animation-name: ${exitKeyframe};
100 animation-duration: ${DURATION}ms;
101 animation-timing-function: ${({
102 theme
103}) => theme.transitions.easing.easeInOut};
104 }
105
106 & .${touchRippleClasses.childPulsate} {
107 position: absolute;
108 /* @noflip */
109 left: 0px;
110 top: 0;
111 animation-name: ${pulsateKeyframe};
112 animation-duration: 2500ms;
113 animation-timing-function: ${({
114 theme
115}) => theme.transitions.easing.easeInOut};
116 animation-iteration-count: infinite;
117 animation-delay: 200ms;
118 }
119`;
120/**
121 * @ignore - internal component.
122 *
123 * TODO v5: Make private
124 */
125
126const TouchRipple = /*#__PURE__*/React.forwardRef(function TouchRipple(inProps, ref) {
127 const props = useThemeProps({
128 props: inProps,
129 name: 'MuiTouchRipple'
130 });
131
132 const {
133 center: centerProp = false,
134 classes = {},
135 className
136 } = props,
137 other = _objectWithoutPropertiesLoose(props, _excluded);
138
139 const [ripples, setRipples] = React.useState([]);
140 const nextKey = React.useRef(0);
141 const rippleCallback = React.useRef(null);
142 React.useEffect(() => {
143 if (rippleCallback.current) {
144 rippleCallback.current();
145 rippleCallback.current = null;
146 }
147 }, [ripples]); // Used to filter out mouse emulated events on mobile.
148
149 const ignoringMouseDown = React.useRef(false); // We use a timer in order to only show the ripples for touch "click" like events.
150 // We don't want to display the ripple for touch scroll events.
151
152 const startTimer = React.useRef(null); // This is the hook called once the previous timeout is ready.
153
154 const startTimerCommit = React.useRef(null);
155 const container = React.useRef(null);
156 React.useEffect(() => {
157 return () => {
158 clearTimeout(startTimer.current);
159 };
160 }, []);
161 const startCommit = React.useCallback(params => {
162 const {
163 pulsate,
164 rippleX,
165 rippleY,
166 rippleSize,
167 cb
168 } = params;
169 setRipples(oldRipples => [...oldRipples, /*#__PURE__*/_jsx(TouchRippleRipple, {
170 classes: {
171 ripple: clsx(classes.ripple, touchRippleClasses.ripple),
172 rippleVisible: clsx(classes.rippleVisible, touchRippleClasses.rippleVisible),
173 ripplePulsate: clsx(classes.ripplePulsate, touchRippleClasses.ripplePulsate),
174 child: clsx(classes.child, touchRippleClasses.child),
175 childLeaving: clsx(classes.childLeaving, touchRippleClasses.childLeaving),
176 childPulsate: clsx(classes.childPulsate, touchRippleClasses.childPulsate)
177 },
178 timeout: DURATION,
179 pulsate: pulsate,
180 rippleX: rippleX,
181 rippleY: rippleY,
182 rippleSize: rippleSize
183 }, nextKey.current)]);
184 nextKey.current += 1;
185 rippleCallback.current = cb;
186 }, [classes]);
187 const start = React.useCallback((event = {}, options = {}, cb) => {
188 const {
189 pulsate = false,
190 center = centerProp || options.pulsate,
191 fakeElement = false // For test purposes
192
193 } = options;
194
195 if (event?.type === 'mousedown' && ignoringMouseDown.current) {
196 ignoringMouseDown.current = false;
197 return;
198 }
199
200 if (event?.type === 'touchstart') {
201 ignoringMouseDown.current = true;
202 }
203
204 const element = fakeElement ? null : container.current;
205 const rect = element ? element.getBoundingClientRect() : {
206 width: 0,
207 height: 0,
208 left: 0,
209 top: 0
210 }; // Get the size of the ripple
211
212 let rippleX;
213 let rippleY;
214 let rippleSize;
215
216 if (center || event === undefined || event.clientX === 0 && event.clientY === 0 || !event.clientX && !event.touches) {
217 rippleX = Math.round(rect.width / 2);
218 rippleY = Math.round(rect.height / 2);
219 } else {
220 const {
221 clientX,
222 clientY
223 } = event.touches && event.touches.length > 0 ? event.touches[0] : event;
224 rippleX = Math.round(clientX - rect.left);
225 rippleY = Math.round(clientY - rect.top);
226 }
227
228 if (center) {
229 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.
230
231 if (rippleSize % 2 === 0) {
232 rippleSize += 1;
233 }
234 } else {
235 const sizeX = Math.max(Math.abs((element ? element.clientWidth : 0) - rippleX), rippleX) * 2 + 2;
236 const sizeY = Math.max(Math.abs((element ? element.clientHeight : 0) - rippleY), rippleY) * 2 + 2;
237 rippleSize = Math.sqrt(sizeX ** 2 + sizeY ** 2);
238 } // Touche devices
239
240
241 if (event?.touches) {
242 // check that this isn't another touchstart due to multitouch
243 // otherwise we will only clear a single timer when unmounting while two
244 // are running
245 if (startTimerCommit.current === null) {
246 // Prepare the ripple effect.
247 startTimerCommit.current = () => {
248 startCommit({
249 pulsate,
250 rippleX,
251 rippleY,
252 rippleSize,
253 cb
254 });
255 }; // Delay the execution of the ripple effect.
256
257
258 startTimer.current = setTimeout(() => {
259 if (startTimerCommit.current) {
260 startTimerCommit.current();
261 startTimerCommit.current = null;
262 }
263 }, DELAY_RIPPLE); // We have to make a tradeoff with this value.
264 }
265 } else {
266 startCommit({
267 pulsate,
268 rippleX,
269 rippleY,
270 rippleSize,
271 cb
272 });
273 }
274 }, [centerProp, startCommit]);
275 const pulsate = React.useCallback(() => {
276 start({}, {
277 pulsate: true
278 });
279 }, [start]);
280 const stop = React.useCallback((event, cb) => {
281 clearTimeout(startTimer.current); // The touch interaction occurs too quickly.
282 // We still want to show ripple effect.
283
284 if (event?.type === 'touchend' && startTimerCommit.current) {
285 startTimerCommit.current();
286 startTimerCommit.current = null;
287 startTimer.current = setTimeout(() => {
288 stop(event, cb);
289 });
290 return;
291 }
292
293 startTimerCommit.current = null;
294 setRipples(oldRipples => {
295 if (oldRipples.length > 0) {
296 return oldRipples.slice(1);
297 }
298
299 return oldRipples;
300 });
301 rippleCallback.current = cb;
302 }, []);
303 React.useImperativeHandle(ref, () => ({
304 pulsate,
305 start,
306 stop
307 }), [pulsate, start, stop]);
308 return /*#__PURE__*/_jsx(TouchRippleRoot, _extends({
309 className: clsx(touchRippleClasses.root, classes.root, className),
310 ref: container
311 }, other, {
312 children: /*#__PURE__*/_jsx(TransitionGroup, {
313 component: null,
314 exit: true,
315 children: ripples
316 })
317 }));
318});
319process.env.NODE_ENV !== "production" ? TouchRipple.propTypes = {
320 /**
321 * If `true`, the ripple starts at the center of the component
322 * rather than at the point of interaction.
323 */
324 center: PropTypes.bool,
325
326 /**
327 * Override or extend the styles applied to the component.
328 * See [CSS API](#css) below for more details.
329 */
330 classes: PropTypes.object,
331
332 /**
333 * @ignore
334 */
335 className: PropTypes.string
336} : void 0;
337export default TouchRipple;
\No newline at end of file