Skip to content

Commit b26dd56

Browse files
authored
Update ReturnToTop.tsx with scrollbar (JhaSourav07#1671)
* Update ReturnToTop.tsx * Update ReturnToTop.tsx * fixed * Update ReturnToTop.tsx * Update ReturnToTop.test.tsx * Update ReturnToTop.test.tsx
1 parent d03a482 commit b26dd56

2 files changed

Lines changed: 71 additions & 16 deletions

File tree

components/ReturnToTop.test.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import { screen, fireEvent } from '@testing-library/react';
77
vi.mock('framer-motion', () => ({
88
AnimatePresence: ({ children }: { children: React.ReactNode }) => children,
99
motion: {
10-
button: ({ children, ...props }: React.ComponentProps<'button'>) => (
10+
button: ({ children, ...props }: { children: React.ReactNode; [key: string]: unknown }) => (
1111
<button {...props}>{children}</button>
1212
),
13+
div: ({ children, ...props }: { children: React.ReactNode; [key: string]: unknown }) => (
14+
<div {...props}>{children}</div>
15+
),
1316
},
1417
}));
1518

@@ -100,7 +103,9 @@ describe('ReturnToTop', () => {
100103

101104
render(<ReturnToTop />);
102105

103-
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
106+
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function), {
107+
passive: true,
108+
});
104109
});
105110

106111
it('removes scroll event listener on unmount', () => {

components/ReturnToTop.tsx

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,26 @@ import { ChevronUp } from 'lucide-react';
66

77
export default function ReturnToTop() {
88
const [isVisible, setIsVisible] = useState(false);
9+
const [scrollProgress, setScrollProgress] = useState(0);
910

1011
useEffect(() => {
1112
const toggleVisibility = () => {
12-
// Show button when user is near the bottom of the page
1313
const scrollHeight = document.documentElement.scrollHeight;
1414
const scrollTop = window.scrollY;
1515
const clientHeight = window.innerHeight;
1616

17-
// Show when user is within 300px of the bottom
18-
if (scrollHeight - (scrollTop + clientHeight) < 300) {
19-
setIsVisible(true);
20-
} else {
21-
setIsVisible(false);
22-
}
17+
// Existing visibility logic
18+
setIsVisible(scrollHeight - (scrollTop + clientHeight) < 300);
19+
20+
// Scroll progress calculation
21+
const progress = (scrollTop / (scrollHeight - clientHeight)) * 100;
22+
23+
setScrollProgress(Math.min(Math.max(progress, 0), 100));
2324
};
2425

25-
window.addEventListener('scroll', toggleVisibility);
26+
toggleVisibility();
27+
window.addEventListener('scroll', toggleVisibility, { passive: true });
28+
2629
return () => window.removeEventListener('scroll', toggleVisibility);
2730
}, []);
2831

@@ -33,20 +36,67 @@ export default function ReturnToTop() {
3336
});
3437
};
3538

39+
// SVG ring values
40+
const radius = 24;
41+
const circumference = 2 * Math.PI * radius;
42+
43+
const strokeDashoffset = circumference - (scrollProgress / 100) * circumference;
44+
3645
return (
3746
<AnimatePresence>
3847
{isVisible && (
39-
<motion.button
48+
<motion.div
4049
initial={{ opacity: 0, y: 20 }}
4150
animate={{ opacity: 1, y: 0 }}
4251
exit={{ opacity: 0, y: 20 }}
4352
transition={{ duration: 0.3 }}
44-
onClick={scrollToTop}
45-
className="fixed bottom-8 right-8 p-3 rounded-full border border-emerald-500/20 bg-white text-emerald-600 hover:bg-emerald-500 hover:text-white hover:border-emerald-500 dark:border-emerald-400/20 dark:bg-black dark:text-emerald-400 dark:hover:bg-emerald-400 dark:hover:text-black dark:hover:border-emerald-400 hover:scale-110 active:scale-95 shadow-[0_4px_20px_rgba(16,185,129,0.15)] dark:shadow-[0_4px_30px_rgba(16,185,129,0.3)] transition-all duration-300 z-50 flex items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00ffaa] focus-visible:ring-offset-2"
46-
aria-label="Return to top"
53+
className="fixed bottom-8 right-8 z-50"
4754
>
48-
<ChevronUp size={24} />
49-
</motion.button>
55+
<div className="relative flex items-center justify-center">
56+
{/* Progress Ring */}
57+
<svg className="absolute -rotate-90" width="64" height="64" aria-hidden="true">
58+
{/* Track */}
59+
<circle
60+
cx="32"
61+
cy="32"
62+
r={radius}
63+
fill="none"
64+
stroke="currentColor"
65+
strokeWidth="3"
66+
className="text-emerald-500/15 dark:text-emerald-400/15"
67+
/>
68+
69+
{/* Progress */}
70+
<circle
71+
cx="32"
72+
cy="32"
73+
r={radius}
74+
fill="none"
75+
stroke="currentColor"
76+
strokeWidth="3"
77+
strokeLinecap="round"
78+
className="text-emerald-500 dark:text-emerald-400 transition-all duration-100"
79+
strokeDasharray={circumference}
80+
strokeDashoffset={strokeDashoffset}
81+
/>
82+
</svg>
83+
84+
{/* Existing Button */}
85+
<button
86+
onClick={scrollToTop}
87+
className={
88+
'relative p-3 rounded-full border border-emerald-500/20 bg-white ' +
89+
'text-emerald-600 hover:bg-emerald-500 hover:text-white ' +
90+
'hover:border-emerald-500 dark:border-emerald-400/20 ' +
91+
'dark:bg-slate-900 dark:text-emerald-400 dark:hover:bg-emerald-600 ' +
92+
'dark:hover:text-white transition-colors duration-200'
93+
}
94+
aria-label="Return to top"
95+
>
96+
<ChevronUp size={24} />
97+
</button>
98+
</div>
99+
</motion.div>
50100
)}
51101
</AnimatePresence>
52102
);

0 commit comments

Comments
 (0)