Shared/Link
Button Style Link
Default
External Link
Invert Color Link
Navigation Links
With Active Class
link.tsx
import type { FunctionComponent, ComponentChildren } from 'preact';
import { usePathname } from '@/src/hooks/use-pathname';
import { usePageLoadingState } from './page-loading-state-provider';
interface LinkProps {
children: ComponentChildren;
to: string;
activeClassName?: string;
partiallyActive?: boolean;
className?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onClick?: (event: any) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
// Link component with active-state detection and loading behavior
const Link: FunctionComponent<LinkProps> = ({
children,
to,
activeClassName,
partiallyActive,
className = '',
...other
}) => {
const pathname = usePathname();
const { isPageLoading, setIsPageLoading } = usePageLoadingState();
// Extract onClick handler from props to preserve it
const { onClick, ...rest } = other;
// Check if link is internal (starts with / but not //)
const internal = /^\/(?!\/)/.test(to);
// Check if link is active
const isActive = partiallyActive ? pathname.startsWith(to) : pathname === to;
const finalClassName =
isActive && activeClassName ? `${className} ${activeClassName}` : className;
if (internal) {
return (
<a
href={to}
className={finalClassName}
onClick={(event: MouseEvent) => {
// Call the original onClick handler first (if provided)
if (onClick && typeof onClick === 'function') {
onClick(event);
}
// If event was cancelled by the onClick handler, stop here
if (event.defaultPrevented) {
return;
}
// Allow default browser behavior for meta/ctrl + click (open in new tab)
if (event.metaKey || event.ctrlKey) {
return;
}
// Prevent click if page is already loading
if (isPageLoading) {
event.preventDefault();
return;
}
// Skip loading state for same-path navigation (already on this page)
if (pathname === to) {
return;
}
// Scroll to top BEFORE navigation (while overlay is shown)
// Use auto behavior to override CSS smooth scroll
// This ensures scroll happens instantly before new content is visible
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
setIsPageLoading(true);
}}
{...rest}
>
{children}
</a>
);
}
// External link
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<a href={to} className={finalClassName} rel="nofollow noopener noreferrer" {...(rest as any)}>
{children}
</a>
);
};
export { Link };
link.stories.tsx
import { Link } from './link';
import type { ControlsMap } from '../../sub-packages/styleguide-v2/src/data/control-types';
/**
* Link Component
*
* A link component that handles both internal and external links.
* Features:
* - Automatic internal/external detection
* - Active state styling with activeClassName
* - Partial matching support for navigation
* - Page loading state integration
* - External links with nofollow/noopener
*/
export const meta = {
title: 'Shared/Link',
};
export const controls: ControlsMap = {
label: { type: 'text', default: '製品一覧を見る' },
href: { type: 'text', default: '/products/' },
};
/**
* Default - Internal link
*/
export const Default = {
args: { label: '製品一覧を見る', href: '/products/' },
render: ({ label, href }: { label: string; href: string }) => (
<Link to={href} className="text-zd-link hover:underline">
{label}
</Link>
),
};
/**
* External link
*/
export const ExternalLink = () => (
<Link to="https://example.com" className="text-zd-link hover:underline">
外部サイトへ
</Link>
);
/**
* With active class styling
*/
export const WithActiveClass = () => (
<Link to="/notes/" className="text-zd-white" activeClassName="text-zd-link font-bold">
記事一覧
</Link>
);
/**
* Navigation style links
*/
export const NavigationLinks = () => (
<nav className="flex gap-hgap-md">
<Link to="/" className="text-zd-white hover:text-zd-link transition-colors">
ホーム
</Link>
<Link to="/products/" className="text-zd-white hover:text-zd-link transition-colors">
製品
</Link>
<Link to="/brands/" className="text-zd-white hover:text-zd-link transition-colors">
ブランド
</Link>
<Link to="/notes/" className="text-zd-white hover:text-zd-link transition-colors">
記事
</Link>
<Link to="/contact/" className="text-zd-white hover:text-zd-link transition-colors">
お問い合わせ
</Link>
</nav>
);
/**
* Invert color link style (used in breadcrumbs)
*/
export const InvertColorLink = () => (
<Link to="/brands/" className="zd-invert-color-link">
ブランド一覧
</Link>
);
/**
* Button-style link
*/
export const ButtonStyleLink = () => (
<Link
to="/contact/"
className="inline-block px-hgap-md py-vgap-sm bg-zd-link text-zd-white rounded hover:brightness-110 transition-all"
>
お問い合わせ
</Link>
);