Last active
January 30, 2025 09:37
-
-
Save manabuyasuda/e9960e978e0048a38dab6bc0e2cdfc62 to your computer and use it in GitHub Desktop.
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
/** | |
* 特定のスクロール位置(要素)を通過したかどうかを監視するカスタムフックです。 | |
* スクロール位置と要素の位置を比較して、要素を通過したかどうかを判定します。 | |
* @param {string} selector 監視対象の要素を特定するCSSセレクタ | |
* @param {Object} options 監視オプション | |
* @param {string} options.scrollBasePosition スクロール量の基準位置を画面の上端('top')または下端('bottom')とするか | |
* @param {string} options.elementBasePosition 要素の基準位置を要素の上端('top')または下端('bottom')とするか | |
* @param {boolean} options.once 一度要素を通過したら監視を終了するかどうか | |
* @returns {boolean} true: スクロール量が要素の位置を超えた, false: まだ要素の位置まで到達していない | |
*/ | |
export function useScrollPastElement( | |
selector: string, | |
options: { | |
scrollBasePosition?: 'top' | 'bottom'; | |
elementBasePosition?: 'top' | 'bottom'; | |
once?: boolean; | |
} = { | |
scrollBasePosition: 'top', | |
elementBasePosition: 'bottom', | |
once: false, | |
}, | |
): boolean { | |
// 要素を通過したかどうか | |
const [hasPassed, setHasPassed] = useState<boolean>(false); | |
// 一度だけ監視する場合は通過後にtrueにして監視を終了する | |
const hasPassedOnce = useRef<boolean>(false); | |
// スクロールイベントの連続発火を制御するフラグ | |
const ticking = useRef<boolean>(false); | |
// 監視対象の要素を取得してメモ化する | |
const getTargetElement = useCallback((): HTMLElement | null => { | |
return document.querySelector(selector) as HTMLElement; | |
}, [selector]); | |
useEffect(() => { | |
const handleScroll = () => { | |
// 監視対象の要素が存在しない場合は処理をスキップする | |
const targetElement = getTargetElement(); | |
if (!targetElement) { | |
return; | |
} | |
// 一度だけの判定オプションが有効で、すでに通過している場合は処理をスキップする | |
if (options.once && hasPassedOnce.current) { | |
return; | |
} | |
// 処理が実行されていない場合だけ実行する | |
if (!ticking.current) { | |
requestAnimationFrame(() => { | |
// スクロール基準位置を計算する(画面上端または下端) | |
const currentScrollY = | |
options.scrollBasePosition === 'bottom' | |
? window.scrollY + window.innerHeight | |
: window.scrollY; | |
// 要素の絶対位置を取得する | |
const rect = targetElement.getBoundingClientRect(); | |
// 要素の絶対位置を計算する | |
// `getBoundingClientRect()`はビューポートを基準にした相対値のため、`window.scrollY`を足して絶対値に変換する | |
const elementPosition = | |
window.scrollY + | |
(options.elementBasePosition === 'bottom' ? rect.bottom : rect.top); | |
// スクロール位置が要素の位置を超えているかを判定する | |
const isPassed = currentScrollY >= elementPosition; | |
// 要素を通過した状態を更新する | |
setHasPassed(isPassed); | |
// 一度だけの判定オプションが有効で、要素を通過した場合は監視を終了 | |
if (options.once && isPassed) { | |
hasPassedOnce.current = true; | |
window.removeEventListener('scroll', handleScroll); | |
} | |
// 処理を完了し、次のイベントを受付可能にする | |
ticking.current = false; | |
}); | |
// 状態を処理中に更新して、これ以降のイベントをブロックする | |
ticking.current = true; | |
} | |
}; | |
// Scroll Junkを防止してパフォーマンスを最適化する | |
window.addEventListener('scroll', handleScroll, { passive: true }); | |
// 初期状態をチェックする | |
handleScroll(); | |
return () => window.removeEventListener('scroll', handleScroll); | |
}, [options, getTargetElement]); | |
return hasPassed; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment