Skip to content

Instantly share code, notes, and snippets.

@sladg
Created July 27, 2022 14:12
Show Gist options
  • Save sladg/a89058899dbd5de3529106711862e815 to your computer and use it in GitHub Desktop.
Save sladg/a89058899dbd5de3529106711862e815 to your computer and use it in GitHub Desktop.
Div wrapper with improved functions for mobile and desktop
import React, { PropsWithChildren, useEffect, useRef, CSSProperties } from 'react'
import { logger } from 'utils/logger'
export type CustomMouseEvent = React.MouseEvent<HTMLDivElement, MouseEvent>
export type CustomTouchEvent = React.TouchEvent<HTMLDivElement>
const MOUSE_MOVE_DELTA = 20
const TOUCH_MOVE_DELTA = 30
const HOLD_TRESHOLD = 500
const DUPLICATE_TRESHOLD = 80
const DOUBLE_CLICK_MAX_DIFF = 250
export interface AmazingDivProps {
onTap?: (key: string, e: CustomTouchEvent) => void
onClick?: (key: string, e: CustomMouseEvent) => void
onHold?: (key: string, e: CustomTouchEvent | CustomMouseEvent) => void
onDoubleClick?: (key: string, e: CustomMouseEvent) => void
}
interface Props {
onTap?: (e: CustomTouchEvent) => void
onClick?: (e: CustomMouseEvent) => void
onDoubleClick?: (e: CustomMouseEvent) => void
onHold?: (e: CustomTouchEvent | CustomMouseEvent) => void
style?: CSSProperties
id: string
}
interface CoordinateType {
x: number
y: number
}
const AmazingDiv = ({ children, onClick, onHold, onTap, onDoubleClick, id, style = {} }: PropsWithChildren<Props>) => {
const longPressInterval = useRef<NodeJS.Timeout>()
const event = useRef<CustomMouseEvent | CustomTouchEvent>()
const startTime = useRef<number>()
const lastEventTime = useRef<number>()
const lastClickTime = useRef<number>()
const startCoordinates = useRef<CoordinateType>(null)
const startLongPressChecker = () => {
longPressInterval.current = setInterval(() => {
logger.debug('[longPress]')
if (startTime.current && isLongPress()) {
logger.debug('[longPress] - activated!')
onHold && onHold(event.current)
handleClean()
}
}, 50)
}
const handleInit = (x: number, y: number, e: CustomMouseEvent | CustomTouchEvent) => {
e.persist()
startCoordinates.current = { x, y }
startTime.current = Date.now()
event.current = e
startLongPressChecker()
}
const handleClean = () => {
longPressInterval.current && clearInterval(longPressInterval.current)
startCoordinates.current = null
startTime.current = null
event.current = null
lastClickTime.current = Date.now()
}
const isDuplicatedEvent = (e: CustomMouseEvent | CustomTouchEvent) => {
const prevTime = lastEventTime.current
const nowTime = e.timeStamp
return nowTime - prevTime < DUPLICATE_TRESHOLD
}
const isLongPress = () => {
const currentTime = Date.now()
const diff = currentTime - startTime.current
return diff > HOLD_TRESHOLD
}
const isSwipe = (e: CustomTouchEvent) => {
if (!startCoordinates.current) return false
if (!e.touches[0]) return false
const xDiff = Math.abs(startCoordinates.current.x - e.touches[0].clientX)
const yDiff = Math.abs(startCoordinates.current.y - e.touches[0].clientY)
return xDiff > TOUCH_MOVE_DELTA || yDiff > TOUCH_MOVE_DELTA
}
const isDrag = (e: CustomMouseEvent) => {
if (!startCoordinates.current) return false
const xDiff = Math.abs(startCoordinates.current.x - e.clientX)
const yDiff = Math.abs(startCoordinates.current.y - e.clientY)
return xDiff > MOUSE_MOVE_DELTA || yDiff > MOUSE_MOVE_DELTA
}
const isDoubleClick = (e: CustomMouseEvent) => {
logger.debug('[isDoubleClick]', lastClickTime.current)
if (!lastClickTime) return false
const currentTime = Date.now()
const diff = currentTime - lastClickTime.current
return diff < DOUBLE_CLICK_MAX_DIFF
}
const handleTouchStart = (e: CustomTouchEvent) => {
logger.debug('[touchStart]')
if (isDuplicatedEvent(e)) {
handleClean()
} else {
handleInit(e.touches[0]?.clientX, e.touches[0]?.clientY, e)
}
}
const handleTouchEnd = (e: CustomTouchEvent) => {
logger.debug('[touchEnd]')
if (isLongPress() || !startTime.current) return
onTap && onTap(event.current as CustomTouchEvent)
handleClean()
lastEventTime.current = e.timeStamp
}
const handleTouchCancel = (e: CustomTouchEvent) => {
logger.debug('[touchCancel]')
handleClean()
}
const handleTouchMove = (e: CustomTouchEvent) => {
logger.debug('[touchMove]')
if (isSwipe(e)) {
handleClean()
}
}
const handleMouseDown = (e: CustomMouseEvent) => {
logger.debug('[mouseDown]', onDoubleClick)
if (isDoubleClick(e)) {
onDoubleClick && onDoubleClick(e)
} else if (isDuplicatedEvent(e)) {
handleClean()
} else {
handleInit(e.clientX, e.clientY, e)
}
}
const handleMouseEnd = (e: CustomMouseEvent) => {
logger.debug('[mouseEnd]')
if (isLongPress() || !startTime.current) return
onClick && onClick(event.current as CustomMouseEvent)
handleClean()
}
const handleMouseLeave = (e: CustomMouseEvent) => {
logger.debug('[mouseLeave]')
handleClean()
}
const handleMouseMove = (e: CustomMouseEvent) => {
if (isDrag(e)) {
logger.debug('[mouseMove] - cleaning!')
handleClean()
}
}
// Ensures that interval does not hang.
useEffect(() => {
!event.current && longPressInterval.current && clearInterval(longPressInterval.current)
}, [event.current])
return (
<div
onTouchStart={handleTouchStart}
onTouchCancel={handleTouchCancel}
onTouchEnd={handleTouchEnd}
onTouchMove={handleTouchMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseEnd}
onMouseLeave={handleMouseLeave}
onMouseMove={handleMouseMove}
style={style}
id={id}
>
{children}
</div>
)
}
export default React.memo(AmazingDiv)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment