-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add useHover hook * add story with temp import * post review fixes * more post review fixes
- Loading branch information
Showing
4 changed files
with
144 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import React, { useCallback, useMemo, useRef, useState } from 'react'; | ||
import { useHover } from "@nanlabs/react-hooks"; | ||
|
||
export const Example = () => { | ||
const [isMemoEnabled, setIsMemoEnabled] = useState(false); | ||
const [isRefTarget, setIsRefTarget] = useState(false); | ||
const [events, setEvents] = useState<string[]>([]) | ||
|
||
const getEventHandler = useCallback((eventName: string) => { | ||
return () => setEvents((events) => [...events, `"${eventName}" callback fired`]) | ||
}, [setEvents]) | ||
|
||
const notMemoizedCallbacks = { | ||
onChange: getEventHandler('onChange'), | ||
onLeave: getEventHandler('onLeave'), | ||
onEnter: getEventHandler('onEnter'), | ||
} | ||
|
||
const memoizedCallbacks = useMemo(() => ({...notMemoizedCallbacks}), [getEventHandler]) | ||
|
||
const divRef = useRef<HTMLDivElement | null>(null) | ||
const getElement = () => divRef.current; | ||
|
||
const isHovered = useHover( | ||
isRefTarget ? divRef : getElement, | ||
isMemoEnabled ? memoizedCallbacks : notMemoizedCallbacks | ||
); | ||
|
||
const hoverableStyles = { | ||
width: 200, | ||
height: 200, | ||
backgroundColor: isHovered ? 'tomato' : 'rebeccapurple', | ||
color: isHovered ? 'black' : 'white', | ||
display: 'flex', | ||
flexDirection: 'column' as const, | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
gap: 10 | ||
} | ||
|
||
return <div style={{display: 'flex', gap: 10}}> | ||
<div style={hoverableStyles} ref={divRef}> | ||
<h2> | ||
{isHovered ? 'Hovered' : 'Hover me'} | ||
</h2> | ||
|
||
<span> | ||
<button onClick={() => setIsMemoEnabled((val) => !val)}> | ||
{isMemoEnabled ? 'Callbacks memoized' : 'Callbacks change each render'} | ||
</button> | ||
</span> | ||
|
||
<span> | ||
<button onClick={() => setIsRefTarget((val) => !val)}> | ||
{isRefTarget ? 'Target provided as ref' : 'Target provided as function'} | ||
</button> | ||
</span> | ||
</div> | ||
<div> Event log: | ||
<ol> | ||
{events.map((event, i) => <li key={`${event}-${i}`}>{event}</li>)} | ||
</ol> | ||
</div> | ||
</div>; | ||
}; | ||
|
||
export default { | ||
title: 'React Hooks/useHover', | ||
component: useHover, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as useHover } from "./useHover"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { useEffect, type MutableRefObject, useState, useCallback, useRef } from 'react'; | ||
|
||
type TargetValue<T> = T | undefined | null; | ||
|
||
type TargetType = HTMLElement | Element | Window | Document; | ||
|
||
export type HoverTarget<T extends TargetType = Element> = | ||
| (() => TargetValue<T>) | ||
| TargetValue<T> | ||
| MutableRefObject<TargetValue<T>>; | ||
|
||
export interface Options { | ||
onEnter?: () => void; | ||
onLeave?: () => void; | ||
onChange?: (isHovering: boolean) => void; | ||
} | ||
|
||
const useHover = <T extends TargetType = Element>(target: HoverTarget<T>, options?: Options): boolean => { | ||
const [isHovered, setIsHovered] = useState(false); | ||
|
||
const onMouseEnter = useCallback(() => { | ||
const isHovered = true; | ||
setIsHovered(isHovered) | ||
options?.onChange?.(isHovered); | ||
options?.onEnter?.() | ||
}, [options?.onEnter, options?.onChange]) | ||
|
||
const onMouseLeave = useCallback(() => { | ||
const isHovered = false; | ||
setIsHovered(isHovered) | ||
options?.onChange?.(isHovered); | ||
options?.onLeave?.() | ||
}, [options?.onLeave, options?.onChange]) | ||
|
||
const prevCallbacks = useRef({ onMouseEnter, onMouseLeave }); | ||
|
||
useEffect(() => { | ||
if ( | ||
!target || | ||
typeof window === 'undefined' || | ||
window?.matchMedia("(any-hover: none)").matches | ||
) { | ||
return; | ||
} | ||
let targetElement: TargetValue<T>; | ||
|
||
|
||
if (typeof target === 'function') { | ||
targetElement = target() | ||
} else if ('current' in target) { | ||
targetElement = target.current; | ||
} | ||
|
||
if (!targetElement) { | ||
return; | ||
} | ||
|
||
targetElement?.addEventListener('mouseenter', onMouseEnter) | ||
targetElement?.addEventListener('mouseleave', onMouseLeave) | ||
|
||
// clean up previous listeners | ||
prevCallbacks.current = { onMouseEnter, onMouseLeave } | ||
return () => { | ||
targetElement?.removeEventListener('mouseenter', prevCallbacks.current.onMouseEnter) | ||
targetElement?.removeEventListener('mouseleave', prevCallbacks.current.onMouseLeave) | ||
} | ||
}, [target, onMouseEnter, onMouseLeave]) | ||
|
||
return isHovered; | ||
}; | ||
|
||
export default useHover |