Files
pupil.brain.riken.jp/src/common/lib/HashLink.tsx
T
orrisroot c94d272342 feat: migrate from Create React App to Vite
Rebuild the site on the stack the other converted sites share: Vite 8,
React 19, TypeScript 7, react-router 8, Biome and sanitize.css, with the
site data moved out of the bundle into static-contents/.

- base the database code on cerebellum.neuroinf.jp and keep the pupil
  platform's table layout, about page and menu
- load the Flickr badge feed as JSONP without extra libraries
- serve the XooNIps data from modules/xoonips/, redirecting the old
  download and /database/file URLs
- restate the column widths as border-box totals and keep form controls
  on white
- track page views with GA4
2026-09-14 17:14:32 +09:00

54 lines
1.6 KiB
TypeScript

import type { ComponentProps, MouseEvent } from 'react';
import { Link, type To } from 'react-router';
type LinkProps = ComponentProps<typeof Link>;
const RETRY_INTERVAL_MS = 16;
const RETRY_COUNT = 20;
const hashOf = (to: To): string => {
if (typeof to === 'string') {
const idx = to.indexOf('#');
return idx === -1 ? '' : to.slice(idx + 1);
}
return (to.hash ?? '').replace(/^#/, '');
};
const scrollToHash = (hash: string, remaining = RETRY_COUNT) => {
if (hash === '') {
return;
}
const id = decodeURIComponent(hash);
const target = document.getElementById(id) ?? document.querySelector(`[name="${CSS.escape(id)}"]`);
if (target !== null) {
target.scrollIntoView();
return;
}
// The target may not be in the DOM yet (pico pages fetch their content).
if (remaining > 0) {
setTimeout(() => {
scrollToHash(hash, remaining - 1);
}, RETRY_INTERVAL_MS);
}
};
/**
* A `<Link>` that also scrolls to the element named by the URL fragment.
* Replaces `react-router-hash-link`, which is unmaintained and still targets react-router v5.
*/
const HashLink = ({ onClick, to, ...rest }: LinkProps) => {
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
onClick?.(e);
if (e.defaultPrevented) {
return;
}
// Let react-router commit the navigation before looking for the target.
setTimeout(() => {
scrollToHash(hashOf(to));
}, 0);
};
return <Link {...rest} to={to} onClick={handleClick} />;
};
export default HashLink;