Files
plane/packages/hooks/src/use-hash-scroll.ts

129 lines
3.6 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect, useState } from "react";
type TArgs = {
elementId: string;
pathname: string;
scrollDelay?: number;
};
type TReturnType = {
isHashMatch: boolean;
hashIds: string[];
scrollToElement: () => boolean;
};
/**
* Custom hook for handling hash-based scrolling to a specific element
* Supports multiple IDs in URL hash (comma-separated, space-separated, or other delimiters)
*
* @param {TArgs} args - The ID of the element to scroll to
* @returns {TReturnType} Object containing hash match status and scroll function
*/
export const useHashScroll = (args: TArgs): TReturnType => {
const { elementId, pathname, scrollDelay = 200 } = args;
// State to track if the current hash contains the provided element ID
const [isHashMatch, setIsHashMatch] = useState(false);
// State to track all IDs found in the hash
const [hashIds, setHashIds] = useState<string[]>([]);
/**
* Scrolls to the element with the provided ID
* @returns {boolean} - Whether the scroll was successful
*/
const scrollToElement = useCallback((): boolean => {
try {
const element = document.getElementById(elementId);
if (element) {
setTimeout(() => {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}, scrollDelay);
return true;
}
return false;
} catch (error) {
console.warn("Hash scroll error:", error);
return false;
}
}, [elementId, scrollDelay]);
/**
* Extracts multiple IDs from hash string
* Supports various delimiters: comma, space, pipe, semicolon
* @param {string} hashString - The hash part of the URL
* @returns {string[]} - Array of clean ID strings
*/
const extractIdsFromHash = (hashString: string | null): string[] => {
if (!hashString) return [];
// Split by common delimiters and clean up
return hashString
.split(/[,\s|;]+/) // Split by comma, space, pipe, or semicolon
.map((id) => id.trim()) // Remove whitespace
.filter((id) => id.length > 0); // Remove empty strings
};
/**
* Get current hash from window.location
* @returns {string | null} - Current hash without the # symbol
*/
const getCurrentHash = (): string | null => {
if (typeof window === "undefined") return null;
const hash = window.location.hash;
return hash ? hash.slice(1) : null; // Remove the # symbol
};
// Effect to handle hash changes and initial load
useEffect(() => {
if (!elementId) {
setIsHashMatch(false);
setHashIds([]);
return;
}
const handleHashChange = () => {
const hash = getCurrentHash();
// Extract all IDs from the hash
const idsInHash = extractIdsFromHash(hash);
setHashIds(idsInHash);
// Check if provided element ID is present in the hash
const hashMatches = idsInHash.includes(elementId);
setIsHashMatch(hashMatches);
// If hash matches, attempt to scroll to the element
if (hashMatches) {
scrollToElement();
}
};
// Handle initial load
handleHashChange();
// Listen for hash changes
window.addEventListener("hashchange", handleHashChange);
return () => {
window.removeEventListener("hashchange", handleHashChange);
};
}, [elementId, pathname, scrollToElement]); // Include pathname to handle route changes
// Return object with hash match status and utility functions
return {
// Whether the current URL hash contains the provided element ID
isHashMatch,
// Array of all IDs found in the current hash
hashIds,
// Manually trigger scroll to the element
scrollToElement,
};
};