Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | 1x 1x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 21x 21x 21x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 1x | import { ChevronUp } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
interface ScrollToTopProps {
/** Show the button after scrolling down this many pixels */
showThreshold?: number;
/** CSS class name for additional styling */
className?: string;
/** Position of the button */
position?: 'bottom-right' | 'bottom-left' | 'fixed-bottom-right';
/** CSS selector for the scroll container (defaults to app layout main) */
targetSelector?: string;
}
const ScrollToTop = ({
showThreshold = 200,
className = '',
position = 'bottom-right',
targetSelector = 'main.overflow-y-auto'
}: ScrollToTopProps) => {
const [isVisible, setIsVisible] = useState(false);
const scrollTargetsRef = useRef<Array<HTMLElement | Window>>([]);
useEffect(() => {
// Resolve potential scroll containers
const explicit = (document.querySelector(targetSelector) as HTMLElement) || null;
const mainEl = (document.querySelector('main.overflow-y-auto') as HTMLElement) || null;
const overflowEls = Array.from(document.querySelectorAll('.overflow-auto, .overflow-y-auto')) as HTMLElement[];
const uniqueTargets = new Set<HTMLElement | Window>();
if (explicit) uniqueTargets.add(explicit);
if (mainEl) uniqueTargets.add(mainEl);
overflowEls.forEach(el => uniqueTargets.add(el));
uniqueTargets.add(window);
// Pick scrollable ones, otherwise fall back to window
const scrollables = Array.from(uniqueTargets).filter(t => {
if (t instanceof Window) return true;
const el = t as HTMLElement;
return el.scrollHeight > el.clientHeight;
});
scrollTargetsRef.current = scrollables.length ? scrollables : [window];
const handleScroll = () => {
const maxTop = scrollTargetsRef.current.reduce((max, t) => {
const top = t instanceof Window ? window.scrollY : (t as HTMLElement).scrollTop;
return Math.max(max, top);
}, 0);
setIsVisible(maxTop > showThreshold);
};
// Initial check
handleScroll();
// Attach listeners to all targets
const add = (t: HTMLElement | Window) => {
t.addEventListener('scroll', handleScroll as EventListener, { passive: true } as AddEventListenerOptions);
};
const remove = (t: HTMLElement | Window) => {
t.removeEventListener('scroll', handleScroll as EventListener);
};
scrollTargetsRef.current.forEach(add);
return () => {
scrollTargetsRef.current.forEach(remove);
};
}, [showThreshold, targetSelector]);
const scrollToTop = () => {
const targets = scrollTargetsRef.current.length ? scrollTargetsRef.current : [window];
targets.forEach(t => {
if (t instanceof Window) {
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
try {
(t as HTMLElement).scrollTo({ top: 0, behavior: 'smooth' });
} catch {
(t as HTMLElement).scrollTop = 0;
}
}
});
// Final window/document fallback
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
};
if (!isVisible) {
return null;
}
const positionClasses = {
'bottom-right': 'fixed bottom-6 right-6',
'bottom-left': 'fixed bottom-6 left-6',
'fixed-bottom-right': 'fixed bottom-8 right-8'
};
return (
<button
onClick={scrollToTop}
className={`
${positionClasses[position]}
z-50
flex
items-center
justify-center
w-10
h-10
bg-blue-600
hover:bg-blue-700
text-white
rounded-full
shadow-lg
transition-all
duration-200
ease-in-out
transform
hover:scale-110
focus:outline-none
focus:ring-2
focus:ring-blue-500
focus:ring-offset-2
${className}
`}
aria-label="Scroll to top"
title="Scroll to top"
>
<ChevronUp className="w-5 h-5" />
</button>
);
};
export default ScrollToTop;
|