Created
October 8, 2024 04:39
-
-
Save manabuyasuda/bb3fd534afc97262f1440f36810520d3 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 { useCallback, useEffect, useRef } from 'react'; | |
type ScrollIntoViewOptions = { | |
behavior?: ScrollBehavior; | |
block?: ScrollLogicalPosition; | |
inline?: ScrollLogicalPosition; | |
}; | |
type UseFocusOnHashOptions = { | |
onFocus?: () => void; | |
scrollOptions?: ScrollIntoViewOptions; | |
}; | |
/** | |
* ハッシュに基づいて要素にフォーカスを設定するカスタムフック | |
* | |
* @param options - フックのオプション | |
* @param options.onFocus - フォーカス設定後に呼び出されるコールバック関数 | |
* @param options.scrollOptions - スクロール設定の初期値 | |
* | |
* @returns setFocusTarget関数を含むオブジェクト | |
* | |
* @example | |
* // 基本的な使用方法 | |
* import { useFocusOnHash } from '@root/common/hooks/use-focus-on-hash'; | |
* const { setFocusTarget } = useFocusOnHash(); | |
* | |
* // オプションを指定した使用方法 | |
* const { setFocusTarget } = useFocusOnHash({ | |
* onFocus: () => console.log('Focused'), | |
* scrollOptions: { behavior: 'smooth', block: 'center' } // フック全体のデフォルト値を設定 | |
* }); | |
* | |
* // setFocusTargetの使用例 | |
* const handleClick = (id: string) => (event: React.MouseEvent) => { | |
* // 個別の呼び出しで特定のオプションを上書き | |
* setFocusTarget(id, event, { block: 'start' }); | |
* // その他の処理 | |
* }; | |
* <Link | |
* href={`#${id}`} | |
* onClick={handleClick(id)} | |
* > | |
* {name} | |
* </Link> | |
*/ | |
export const useFocusOnHash = ({ | |
onFocus, | |
scrollOptions, | |
}: UseFocusOnHashOptions = {}) => { | |
const focusTargetRef = useRef<string | null>(null); | |
const isInitialMount = useRef(true); | |
/** | |
* 指定されたIDの要素にフォーカスを設定し、その要素までスクロールする | |
* @param id - フォーカスを設定する要素のID | |
* @param event - Reactのマウスイベント(オプション) | |
* @param overrideScrollOptions - スクロールオプションを上書きするためのオプション(オプション) | |
*/ | |
const setFocusTarget = useCallback( | |
( | |
id: string, | |
event?: React.MouseEvent, | |
overrideScrollOptions?: ScrollIntoViewOptions, | |
) => { | |
// イベントが提供された場合、デフォルトの動作を防止する | |
event?.preventDefault(); | |
focusTargetRef.current = id; | |
// URLのハッシュを更新する | |
window.history.pushState(null, '', `#${id}`); | |
// フォーカスを設定 | |
requestAnimationFrame(() => { | |
const targetElement = document.getElementById(id); | |
if (targetElement) { | |
targetElement.tabIndex = -1; | |
targetElement.scrollIntoView({ | |
behavior: 'auto', // NOTE: `smooth`を指定すると位置がズレる | |
block: 'center', | |
...scrollOptions, | |
...overrideScrollOptions, | |
}); | |
// フォーカス時にはスクロールを発生させない | |
targetElement.focus({ preventScroll: true }); | |
// コールバック関数を呼び出す | |
onFocus?.(); | |
} | |
}); | |
}, | |
[onFocus, scrollOptions], | |
); | |
useEffect(() => { | |
// ハッシュ変更時にフォーカスを設定する | |
const handleHashChange = () => { | |
const hash = window.location.hash.slice(1); | |
if (hash) { | |
setFocusTarget(hash); | |
} | |
}; | |
// 初回マウント時のみ実行する | |
if (isInitialMount.current) { | |
handleHashChange(); | |
isInitialMount.current = false; | |
} | |
// ハッシュ変更イベントリスナーを追加する | |
window.addEventListener('hashchange', handleHashChange); | |
// DOMContentLoadedイベントリスナーを追加する | |
window.addEventListener('DOMContentLoaded', handleHashChange); | |
// クリーンアップ関数を返す | |
return () => { | |
window.removeEventListener('hashchange', handleHashChange); | |
}; | |
}, [setFocusTarget]); | |
return { setFocusTarget }; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment