UNPKG

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