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';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { FiList, FiChevronRight } from 'react-icons/fi';
|
import { FiList, FiChevronRight } from 'react-icons/fi';
|
||||||
import { PostToc } from './post-toc';
|
import { PostToc } from './post-toc';
|
||||||
import { clsx, type ClassValue } from 'clsx';
|
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 }) {
|
export function PostLayout({ children, hasToc = true, contentKey }: { children: React.ReactNode; hasToc?: boolean; contentKey?: string }) {
|
||||||
const [isTocOpen, setIsTocOpen] = useState(hasToc);
|
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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -47,25 +72,8 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Toggle Button (Glassmorphism Pill) */}
|
{/* Toggle Button - Rendered via Portal */}
|
||||||
{hasToc && (
|
{tocButton && createPortal(tocButton, document.body)}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <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
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
Reference in New Issue
Block a user