-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rewrite <HoverLayer> using Hooks #10
Changes from 3 commits
2a52201
f6e7cb3
16933b1
eaa0249
121db52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { | ||
useRef, | ||
useEffect, | ||
useCallback, | ||
} from 'react'; | ||
|
||
export interface AnimationFrameControl { | ||
/** The current animation frame ID */ | ||
animationFrame: number | null; | ||
|
||
/** Function that takes `rafCallback` and put it into `window.requestAnimationFrame()` */ | ||
requestWindowAnimationFrame: (rafCallback: () => void) => void; | ||
} | ||
|
||
export function useAnimationFrame(): AnimationFrameControl { | ||
/** stores the animation frame ID */ | ||
const animaFrameIdRef = useRef<number | null>(null); | ||
|
||
const requestWindowAnimationFrame = useCallback( | ||
(rafCallback) => { | ||
animaFrameIdRef.current = window.requestAnimationFrame(rafCallback); | ||
}, | ||
[], | ||
); | ||
|
||
useEffect( | ||
() => { | ||
// the functional component unmounting | ||
return () => { | ||
// cancel the scheduled update on the animation frame | ||
if (animaFrameIdRef.current) { | ||
window.cancelAnimationFrame(animaFrameIdRef.current); | ||
} | ||
}; | ||
}, | ||
[], | ||
); | ||
|
||
return { | ||
requestWindowAnimationFrame, | ||
animationFrame: animaFrameIdRef.current, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,14 @@ | ||
import * as React from 'react'; | ||
import React, { | ||
FunctionComponent, | ||
useRef, | ||
useEffect, | ||
useCallback, | ||
} from 'react'; | ||
import { throttle } from 'lodash-es'; | ||
import { localPoint } from '@vx/event'; | ||
|
||
import { useAnimationFrame } from '../hooks/useAnimationFrame'; | ||
|
||
import { DataLayerRenderParams } from './DataLayer'; | ||
|
||
export interface HoverLayerProps { | ||
|
@@ -14,72 +21,82 @@ export interface HoverLayerProps { | |
/** Function to hide the tooltip */ | ||
clearHovering: () => void; | ||
|
||
/** Hidden components to detect the mouse or touch interactions */ | ||
/** | ||
* Hidden components to detect the mouse or touch interactions. | ||
* **Note:** The order of the components should correspond to the order of the data. | ||
*/ | ||
collisionComponents: JSX.Element[]; | ||
|
||
/** The debounce time for the mouse and touch events */ | ||
/** The throttle time for the mouse and touch events */ | ||
throttleTime: number; | ||
} | ||
|
||
export class HoverLayer extends React.PureComponent<HoverLayerProps, {}> { | ||
public static defaultProps = { | ||
throttleTime: 180, | ||
handleHover: () => null, | ||
}; | ||
|
||
public animaFrameID: number; | ||
|
||
/** Updates the position of the tooltip and sets the currently active data index */ | ||
private updatePosition = (dataIndex: number, event: React.SyntheticEvent) => { | ||
const { setHoveredPosAndIndex, handleHover } = this.props; | ||
|
||
// custom action which executes before the position is updated | ||
handleHover(); | ||
|
||
// convert the position of the event to the coordinate system of the SVG | ||
const { x, y } = localPoint(event); | ||
this.animaFrameID = window.requestAnimationFrame(() => { | ||
setHoveredPosAndIndex( | ||
dataIndex, | ||
x, | ||
y, | ||
); | ||
export const HoverLayer: FunctionComponent<HoverLayerProps> = ({ | ||
setHoveredPosAndIndex, | ||
handleHover= () => null, | ||
clearHovering, | ||
collisionComponents, | ||
throttleTime = 180, | ||
}) => { | ||
/** use requestAnimationFrame to schedule updates of hovered position and data index */ | ||
const { requestWindowAnimationFrame } = useAnimationFrame(); | ||
|
||
/** Function to update the position of the tooltip and sets the currently active data index */ | ||
const updatePosition = useCallback( | ||
throttle( | ||
(dataIndex: number, event: React.SyntheticEvent) => { | ||
// custom action which executes before the position is updated | ||
handleHover(); | ||
|
||
// convert the position of the event to the coordinate system of the SVG | ||
const { x, y } = localPoint(event); | ||
requestWindowAnimationFrame(() => { | ||
setHoveredPosAndIndex( | ||
dataIndex, | ||
x, | ||
y, | ||
); | ||
}); | ||
}, | ||
throttleTime, | ||
), | ||
[], | ||
); | ||
|
||
/** Function to keep the event data and perform throttled updates of the position */ | ||
const handleTooltip = useCallback( | ||
(dataIndex: number) => (event: React.SyntheticEvent) => { | ||
// removes it from the event pool and allows references to the event | ||
event.persist(); | ||
updatePosition(dataIndex, event); | ||
}, | ||
[], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as the comment above (L63). |
||
); | ||
|
||
/** Function to cancel the update of position and disable the hovering state */ | ||
const hideTooltip = useCallback( | ||
() => { | ||
// cancel the previously thorttled event to prevent the tooltip from reappearing | ||
updatePosition.cancel(); | ||
clearHovering(); | ||
}, | ||
[], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as the comment above with one more |
||
); | ||
|
||
const detectionAreas = collisionComponents.map((area: JSX.Element, dataIndex: number) => { | ||
const handleCurrentTooltip = handleTooltip(dataIndex); | ||
return React.cloneElement(area, { | ||
onTouchStart: handleCurrentTooltip, | ||
onTouchMove: handleCurrentTooltip, | ||
onMouseMove: handleCurrentTooltip, | ||
onMouseLeave: hideTooltip, | ||
}); | ||
}; | ||
|
||
private throttledUpdatePosition = throttle(this.updatePosition, this.props.throttleTime); | ||
|
||
private handleTooltip = (dataIndex: number) => (event: React.SyntheticEvent) => { | ||
// removes it from the event pool and allows references to the event | ||
event.persist(); | ||
this.throttledUpdatePosition(dataIndex, event); | ||
}; | ||
|
||
private hideTooltip = () => { | ||
// cancel the previously thorttled event to prevent the tooltip from reappearing | ||
this.throttledUpdatePosition.cancel(); | ||
this.props.clearHovering(); | ||
}; | ||
|
||
public componentWillUnmount() { | ||
window.cancelAnimationFrame(this.animaFrameID); | ||
} | ||
|
||
public render() { | ||
const { collisionComponents } = this.props; | ||
const detectionAreas = collisionComponents.map((area: JSX.Element, dataIndex: number) => { | ||
const handleCurrentTooltip = this.handleTooltip(dataIndex); | ||
return React.cloneElement(area, { | ||
onTouchStart: handleCurrentTooltip, | ||
onTouchMove: handleCurrentTooltip, | ||
onMouseMove: handleCurrentTooltip, | ||
onMouseLeave: this.hideTooltip, | ||
}); | ||
}); | ||
|
||
return ( | ||
// Render areas to detect collisions of mouse pointers or touches with data points | ||
detectionAreas | ||
); | ||
} | ||
} | ||
}); | ||
|
||
// Render areas to detect collisions of mouse pointers or touches with data points | ||
return ( | ||
<> | ||
{detectionAreas} | ||
</> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we don't need the Fragment, just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is necessary for Typescript because if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting 🤕 I just tried directly |
||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the second args should be
[handleHover, setHoveredPosAndIndex, throttleTime]
. The throttled function should change if one of these props changes. See the note part of useCallback doc.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch. Thanks for finding this out 😃