Last active
January 27, 2025 08:29
-
-
Save manabuyasuda/0cdb84b8b88bb2a0db66dd4dcdbf4ade to your computer and use it in GitHub Desktop.
useClickOutside
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
import { RefObject, useRef, useEffect } from 'react'; | |
/** | |
* イベントタイプ | |
* - mousedown: マウスボタンを押したとき | |
* - click: マウスボタンを押して離したとき | |
* - touchstart: タッチしたとき | |
*/ | |
type EventType = 'mousedown' | 'click' | 'touchstart'; | |
type UseClickOutsideOptions = { | |
/** 検知するイベントタイプ(デフォルト: mousedown) */ | |
eventType?: EventType; | |
/** Escキーでの閉じる機能を有効にするか(デフォルト: true) */ | |
enableEsc?: boolean; | |
/** イベントリスナーのオプション */ | |
listenerOptions?: AddEventListenerOptions; | |
}; | |
type UseClickOutsideReturn<T extends HTMLElement> = { | |
ref: RefObject<T>; | |
}; | |
/** | |
* 要素の外側のクリックとEscキーを検知するカスタムフックです。 | |
* @param handler - 親要素のクリックまたはEscキー押下時のコールバック関数 | |
* @param options - イベントに関する設定 | |
* @returns ref - 監視対象の要素への参照 | |
* @example | |
* const { ref } = useClickOutside(onClose, { | |
* eventType: 'click', | |
* enableEsc: false, | |
* listenerOptions: { | |
* capture: true, | |
* }, | |
* }); | |
* | |
* return ( | |
* <div className={styles.screen}> | |
* <div ref={ref} className={styles.main}> | |
* </div> | |
* </div> | |
* ) | |
*/ | |
export const useClickOutside = <T extends HTMLElement>( | |
handler: () => void, | |
options: UseClickOutsideOptions = {}, | |
): UseClickOutsideReturn<T> => { | |
const ref = useRef<T>(null); | |
// オプションの初期値 | |
const { | |
eventType = 'mousedown', | |
enableEsc = true, | |
listenerOptions, | |
} = options; | |
// handlerが`useCallback`でラップされていない場合でも、イベントリスナーの再登録を防ぐ | |
const savedHandler = useRef(handler); | |
// handlerが変更された場合に更新する | |
useEffect(() => { | |
savedHandler.current = handler; | |
}, [handler]); | |
useEffect(() => { | |
const handleEvent = (event: MouseEvent | TouchEvent) => { | |
// クリックされた要素 | |
const target = event.target as Node; | |
// DOMが削除されていたら終了する | |
if (!target?.isConnected) return; | |
// 自分自身か子孫要素でなければ外側をクリックしたとみなせる | |
const isOutside = ref.current && !ref.current.contains(target); | |
if (isOutside) { | |
// handlerを実行する | |
savedHandler.current(); | |
} | |
}; | |
const handleKeyDown = (event: KeyboardEvent) => { | |
if (event.key === 'Escape') { | |
savedHandler.current(); | |
} | |
}; | |
document.addEventListener(eventType, handleEvent, listenerOptions); | |
if (enableEsc) { | |
document.addEventListener('keydown', handleKeyDown, listenerOptions); | |
} | |
return () => { | |
document.removeEventListener(eventType, handleEvent, listenerOptions); | |
if (enableEsc) { | |
document.removeEventListener('keydown', handleKeyDown, listenerOptions); | |
} | |
}; | |
}, [ref, eventType, enableEsc, listenerOptions]); | |
return { ref }; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment