Skip to content

Instantly share code, notes, and snippets.

@manabuyasuda
Last active January 27, 2025 08:29
Show Gist options
  • Save manabuyasuda/0cdb84b8b88bb2a0db66dd4dcdbf4ade to your computer and use it in GitHub Desktop.
Save manabuyasuda/0cdb84b8b88bb2a0db66dd4dcdbf4ade to your computer and use it in GitHub Desktop.
useClickOutside
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