Skip to content

Commit f417f70

Browse files
committed
ScrollToTop wait for Giscus to load, in progress
1 parent 5f14c52 commit f417f70

File tree

3 files changed

+128
-3
lines changed

3 files changed

+128
-3
lines changed

src/components/Footer.astro

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
import { Icon } from 'astro-icon/components';
33
4-
// import { BackToTop } from '@/components/BackToTop';
54
import Link from '@/components/Link.astro';
5+
import ScrollToTop from '@/components/react/ScrollToTop';
66
import { ROUTES } from '@/constants/routes';
77
import { CONFIG } from '@/config';
88
import { getLatestCommitInfo } from '@/libs/git';
@@ -79,5 +79,8 @@ const trimmedMessage = limitString(message, messageLength);
7979
</ul>
8080
</div>
8181
</div>
82-
{/* <BackToTop client:only="solid-js" /> */}
82+
83+
<ScrollToTop client:only="react">
84+
<Icon name="mdi:arrow-up-thin" class="h-10 w-10 text-content hover:text-links-hover" />
85+
</ScrollToTop>
8386
</footer>

src/components/react/ScrollToTop.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
3+
import { SELECTORS } from '@/constants/dom';
4+
5+
import type { ReactNode } from 'react';
6+
7+
interface Props {
8+
children: ReactNode;
9+
}
10+
11+
const { GISCUS_IFRAME_SELECTOR, GISCUS_WIDGET_SELECTOR } = SELECTORS;
12+
13+
const fixedClasses = ['opacity-1', 'translate-y-0'];
14+
const hiddenClasses = ['opacity-0', 'translate-y-full'];
15+
16+
const showLink = (linkRef: React.RefObject<HTMLAnchorElement>): void => {
17+
linkRef.current?.classList.add(...fixedClasses);
18+
linkRef.current?.classList.remove(...hiddenClasses);
19+
};
20+
21+
const hideLink = (linkRef: React.RefObject<HTMLAnchorElement>): void => {
22+
linkRef.current?.classList.remove(...fixedClasses);
23+
linkRef.current?.classList.add(...hiddenClasses);
24+
};
25+
26+
const getHalfViewportHeight = (window: Window) => Math.floor(window.innerHeight / 2);
27+
const getDocumentScrollHeight = (document: Document) => document.documentElement.scrollHeight;
28+
29+
const ScrollToTop: React.FC<Props> = ({ children }) => {
30+
const linkRef = useRef<HTMLAnchorElement>(null);
31+
const topRef = useRef<HTMLDivElement>(null);
32+
const bottomRef = useRef<HTMLDivElement>(null);
33+
34+
const [height, setHeight] = useState(getHalfViewportHeight(window));
35+
const [scrollHeight, setScrollHeight] = useState(getDocumentScrollHeight(document));
36+
37+
console.log('height', height, 'scrollHeight', scrollHeight);
38+
39+
useEffect(() => {
40+
const callback: IntersectionObserverCallback = (entries) => {
41+
const isAtTopOrBottom = entries.every((entry) => !entry.isIntersecting);
42+
43+
if (linkRef.current) {
44+
isAtTopOrBottom ? showLink(linkRef) : hideLink(linkRef);
45+
}
46+
};
47+
48+
const intersect = new IntersectionObserver(callback);
49+
50+
if (topRef.current) intersect.observe(topRef.current);
51+
if (bottomRef.current) intersect.observe(bottomRef.current);
52+
53+
return () => {
54+
intersect.disconnect();
55+
};
56+
}, []);
57+
58+
useEffect(() => {
59+
const shadowHost = document.querySelector(GISCUS_WIDGET_SELECTOR);
60+
const shadowRoot = shadowHost?.shadowRoot;
61+
if (!shadowRoot) return;
62+
63+
const iframe = shadowRoot.querySelector(GISCUS_IFRAME_SELECTOR) as HTMLIFrameElement;
64+
65+
console.log('iframe', iframe);
66+
if (!iframe) return;
67+
68+
console.log('updated scrollHeight', scrollHeight);
69+
70+
let timer: NodeJS.Timeout;
71+
const handleLoad = () =>
72+
(timer = setTimeout(() => setScrollHeight(getDocumentScrollHeight(document)), 3000));
73+
74+
// not onLoad but MutationObserver to lose class="loading"
75+
iframe.addEventListener('load', handleLoad);
76+
77+
return () => {
78+
clearTimeout(timer);
79+
window.removeEventListener('load', handleLoad);
80+
};
81+
}, []);
82+
83+
// on resize only, vertical...?
84+
useEffect(() => {
85+
const handleResize = () => {
86+
window.requestAnimationFrame(() => setHeight(getHalfViewportHeight(window)));
87+
};
88+
89+
window.addEventListener('resize', handleResize);
90+
91+
return () => {
92+
window.removeEventListener('resize', handleResize);
93+
};
94+
}, []);
95+
96+
return (
97+
<>
98+
<div
99+
ref={topRef}
100+
className="pointer-events-none absolute top-0 w-10 bg-red-500"
101+
style={{ height: `${height}px` }}
102+
/>
103+
<div
104+
ref={bottomRef}
105+
className="pointer-events-none absolute w-10 bg-blue-500"
106+
style={{ height: `${height}px`, top: `${scrollHeight - height}px` }}
107+
/>
108+
<a
109+
ref={linkRef}
110+
id="to-top"
111+
href="#top"
112+
className="z-10 fixed bottom-6 right-6 rounded bg-base-200 border border-base-300"
113+
aria-label="Scroll to top"
114+
>
115+
{/* astro-icon must be passed as slot */}
116+
{children}
117+
</a>
118+
</>
119+
);
120+
};
121+
122+
export default ScrollToTop;

src/utils/strings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const getRandomLengthSubstring = (inputString: string, length: number, ma
66

77
/** for satori og template */
88
export const limitString = (str: string, maxLength: number) =>
9-
str.length > maxLength ? str.slice(0, maxLength) + '...' : str;
9+
str.length > maxLength ? str.slice(0, maxLength).trim() + '...' : str.trim();
1010

1111
export const getRandomElementFromArray = <T>(arr: T[]): T =>
1212
arr[Math.floor(Math.random() * arr.length)];

0 commit comments

Comments
 (0)