Skip to content

Instantly share code, notes, and snippets.

@manabuyasuda
Created October 8, 2024 04:39
Show Gist options
  • Save manabuyasuda/bb3fd534afc97262f1440f36810520d3 to your computer and use it in GitHub Desktop.
Save manabuyasuda/bb3fd534afc97262f1440f36810520d3 to your computer and use it in GitHub Desktop.
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