Skip to content

Commit e3a91d7

Browse files
authored
Improve ripple for touch device (#9)
* Improve ripple for touch device * Update patch for npm deploy
1 parent 26d1f96 commit e3a91d7

File tree

4 files changed

+102
-74
lines changed

4 files changed

+102
-74
lines changed

.changeset/silly-onions-deliver.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@crayon-ui/crayon": patch
3+
---
4+
5+
Improve ripple for touch device

packages/crayon/src/components/Ripple/Ripple.styled.ts

+3-25
Original file line numberDiff line numberDiff line change
@@ -15,49 +15,27 @@ interface RippleEffectProps {
1515
radius: number
1616
cx: number
1717
cy: number
18-
mount?: boolean
18+
timeout: number
1919
}
2020

2121
export const RippleEffect = styled("span")<RippleEffectProps>(
22-
({ theme, color, radius, cx, cy, mount = true }) => css`
22+
({ theme, color, radius, cx, cy, timeout }) => css`
2323
position: absolute;
2424
left: ${cx - radius}px;
2525
top: ${cy - radius}px;
2626
background-color: ${theme.palette[color].light};
2727
border-radius: ${radius}px;
2828
width: ${radius * 2}px;
2929
height: ${radius * 2}px;
30-
animation: ${scaleOut} 500ms ease-in-out, ${mount ? fadeIn : fadeOut} 500ms ease-in-out;
31-
animation-fill-mode: forwards;
30+
animation: ${scaleOut} ${timeout}ms ease-in-out;
3231
`
3332
)
3433

3534
const scaleOut = keyframes`
3635
0% {
3736
transform: scale(0);
3837
}
39-
4038
100% {
4139
transform: scale(1);
4240
}
4341
`
44-
45-
const fadeIn = keyframes`
46-
0% {
47-
opacity: 0.2;
48-
}
49-
50-
100% {
51-
opacity: 0.4;
52-
}
53-
`
54-
55-
export const fadeOut = keyframes`
56-
0% {
57-
opacity: 0.4;
58-
}
59-
60-
100% {
61-
opacity: 0;
62-
}
63-
`

packages/crayon/src/components/Ripple/Ripple.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ interface Props {
1212
}
1313

1414
export const Ripple: FC<Props> = memo(({ children, ...props }) => {
15-
const { bind, ripples } = useRipple(props)
15+
const { bind, ripple } = useRipple(props)
1616
return (
1717
<RippleRoot {...bind()}>
18-
{ripples}
18+
{ripple}
1919
{children}
2020
</RippleRoot>
2121
)
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
11
import { distance } from "@crayon-ui/utils"
2-
import { AnimationEvent, PointerEvent, useCallback, useRef, useState } from "react"
2+
import {
3+
createRef,
4+
DragEventHandler,
5+
FocusEvent,
6+
FocusEventHandler,
7+
MouseEvent,
8+
MouseEventHandler,
9+
Ref,
10+
TouchEvent,
11+
TouchEventHandler,
12+
useCallback,
13+
useEffect,
14+
useRef,
15+
useState
16+
} from "react"
17+
import { TransitionGroup } from "react-transition-group"
318

419
import { useMeasure } from "../../hooks"
520
import { ColorVariant } from "../../theme"
6-
import { fadeOut, RippleEffect } from "./Ripple.styled"
21+
import { TweenTransition } from "../Transition"
22+
import { RippleEffect } from "./Ripple.styled"
23+
24+
const delay = 80
25+
const timeout = 300
26+
const animation = {
27+
timeout,
28+
transition: `opacity ${timeout}ms ease-in-out`,
29+
begin: {
30+
opacity: 0
31+
},
32+
end: {
33+
opacity: 0.4
34+
}
35+
}
36+
37+
interface BindRippleHandlers {
38+
ref: Ref<HTMLElement>
39+
onBlur: FocusEventHandler
40+
onContextMenu: MouseEventHandler
41+
onDragLeave: DragEventHandler
42+
onMouseDown: MouseEventHandler
43+
onMouseLeave: MouseEventHandler
44+
onMouseUp: MouseEventHandler
45+
onTouchEnd: TouchEventHandler
46+
onTouchMove: TouchEventHandler
47+
onTouchStart: TouchEventHandler
48+
}
749

850
interface Props {
951
color?: ColorVariant
@@ -12,76 +54,79 @@ interface Props {
1254
}
1355

1456
export const useRipple = ({ color = "primary", center = false, disabled = false }: Props) => {
15-
const rippleKey = useRef<string | null>()
16-
const { measure, rect } = useMeasure<HTMLSpanElement>()
57+
const { measure, rect } = useMeasure<HTMLElement>()
1758
const [ripples, setRipples] = useState<JSX.Element[]>([])
59+
const isIgnoreMouseDown = useRef<boolean>(false)
60+
const timer = useRef<NodeJS.Timeout>()
1861

19-
const removeRippleByKey = useCallback(
20-
(key: string) => (e: AnimationEvent) => {
21-
if (e.animationName === fadeOut.name) {
22-
setRipples((prev) => prev.filter((effect) => effect.key !== key))
23-
}
62+
useEffect(
63+
() => () => {
64+
timer.current && clearTimeout(timer.current)
2465
},
2566
[]
2667
)
2768

28-
const hide = useCallback(() => {
29-
if (!rippleKey.current) {
30-
return
69+
const commit = useCallback((radius: number, cx: number, cy: number) => {
70+
const ref = createRef<HTMLElement>()
71+
setRipples((prev) => [
72+
...prev,
73+
<TweenTransition {...animation} key={`${performance.now()}`}>
74+
<RippleEffect ref={ref} color={color} radius={radius} cx={cx} cy={cy} timeout={timeout} />
75+
</TweenTransition>
76+
])
77+
}, [])
78+
79+
const stop = useCallback((e: FocusEvent | MouseEvent | TouchEvent) => {
80+
if (e.type === "touchmove") {
81+
timer.current && clearTimeout(timer.current)
3182
}
32-
const key = rippleKey.current
33-
rippleKey.current = null
34-
setRipples((prev) =>
35-
prev.map((ripple) => {
36-
if (ripple.key !== key) {
37-
return ripple
38-
}
39-
return (
40-
<RippleEffect
41-
key={key}
42-
{...ripple.props}
43-
mount={false}
44-
onAnimationEnd={removeRippleByKey(key)}
45-
/>
46-
)
47-
})
48-
)
49-
}, [removeRippleByKey])
83+
setRipples((prev) => (prev.length > 0 ? prev.slice(1) : prev))
84+
}, [])
5085

51-
const show = useCallback(
52-
({ clientX, clientY }: PointerEvent<HTMLSpanElement>) => {
53-
if (rippleKey.current) {
54-
hide()
86+
const start = useCallback(
87+
(event: TouchEvent | MouseEvent) => {
88+
const isTouchEvent = "touches" in event && event.touches.length > 0
89+
if (!isTouchEvent && isIgnoreMouseDown.current) {
90+
isIgnoreMouseDown.current = false
91+
return
5592
}
93+
isIgnoreMouseDown.current = isTouchEvent
94+
5695
const { left, top, width, height } = rect()
96+
const { clientX, clientY } = isTouchEvent ? event.touches[0] : (event as MouseEvent)
5797
const cx = center ? width / 2 : clientX - left
5898
const cy = center ? height / 2 : clientY - top
5999
const radius = distance([0, 0], [Math.max(cx, width - cx), Math.max(cy, height - cy)])
60-
const key = `${performance.now()}`
61-
rippleKey.current = key
62-
setRipples((prev) => [
63-
...prev,
64-
<RippleEffect key={key} color={color} radius={radius} cx={cx} cy={cy} />
65-
])
100+
if (isTouchEvent) {
101+
timer.current = setTimeout(() => commit(radius, cx, cy), delay)
102+
return
103+
}
104+
commit(radius, cx, cy)
66105
},
67-
[hide]
106+
[stop]
68107
)
69108

70-
const bind = useCallback(() => {
109+
const bind = useCallback((): Partial<BindRippleHandlers> => {
71110
if (disabled) {
72111
return measure()
73112
}
74113

75114
return {
76115
...measure(),
77-
onPointerDown: show,
78-
onPointerMove: hide,
79-
onPointerUp: hide
116+
onBlur: stop,
117+
onContextMenu: stop,
118+
onDragLeave: stop,
119+
onMouseDown: start,
120+
onMouseUp: stop,
121+
onMouseLeave: stop,
122+
onTouchStart: start,
123+
onTouchMove: stop,
124+
onTouchEnd: stop
80125
}
81-
}, [measure, show, hide, disabled])
126+
}, [measure, start, stop, disabled])
82127

83128
return {
84129
bind,
85-
ripples
130+
ripple: <TransitionGroup component={null}>{ripples}</TransitionGroup>
86131
}
87132
}

0 commit comments

Comments
 (0)