Fix TOC button to be truly fixed-position using React Portal
The TOC toggle button was appearing near the end of posts instead of floating at a fixed position. This happened because the button was rendered inside the PostLayout component hierarchy. Changes: - Use React Portal to render TOC button at document.body level - Add mounted state for proper SSR/client hydration - Button now floats like back-to-top button, visible from start This ensures the button is always visible and accessible, similar to the back-to-top button behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { FiList, FiChevronRight } from 'react-icons/fi';
|
||||
import { PostToc } from './post-toc';
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
@@ -12,6 +13,30 @@ function cn(...inputs: ClassValue[]) {
|
||||
|
||||
export function PostLayout({ children, hasToc = true, contentKey }: { children: React.ReactNode; hasToc?: boolean; contentKey?: string }) {
|
||||
const [isTocOpen, setIsTocOpen] = useState(hasToc);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const tocButton = hasToc && mounted ? (
|
||||
<button
|
||||
onClick={() => setIsTocOpen(!isTocOpen)}
|
||||
className={cn(
|
||||
"toc-button fixed bottom-8 right-8 z-50 flex items-center gap-2 rounded-full border border-white/20 bg-white/80 px-4 py-2.5 shadow-lg backdrop-blur-md hover:bg-white dark:border-white/10 dark:bg-slate-900/80 dark:hover:bg-slate-900",
|
||||
"text-sm font-medium text-slate-600 dark:text-slate-300",
|
||||
"lg:right-20" // Adjust position for desktop
|
||||
)}
|
||||
aria-label="Toggle Table of Contents"
|
||||
>
|
||||
{isTocOpen ? (
|
||||
<FiChevronRight className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<FiList className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span>{isTocOpen ? 'Hide' : 'Menu'}</span>
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -47,25 +72,8 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children:
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toggle Button (Glassmorphism Pill) */}
|
||||
{hasToc && (
|
||||
<button
|
||||
onClick={() => setIsTocOpen(!isTocOpen)}
|
||||
className={cn(
|
||||
"toc-button fixed bottom-8 right-8 z-50 flex items-center gap-2 rounded-full border border-white/20 bg-white/80 px-4 py-2.5 shadow-lg backdrop-blur-md hover:bg-white dark:border-white/10 dark:bg-slate-900/80 dark:hover:bg-slate-900",
|
||||
"text-sm font-medium text-slate-600 dark:text-slate-300",
|
||||
"lg:right-20" // Adjust position for desktop
|
||||
)}
|
||||
aria-label="Toggle Table of Contents"
|
||||
>
|
||||
{isTocOpen ? (
|
||||
<FiChevronRight className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<FiList className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span>{isTocOpen ? 'Hide' : 'Menu'}</span>
|
||||
</button>
|
||||
)}
|
||||
{/* Toggle Button - Rendered via Portal */}
|
||||
{tocButton && createPortal(tocButton, document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Reference in New Issue
Block a user