Skip to content

Commit 3742f6c

Browse files
committed
feat: to top
Signed-off-by: Innei <[email protected]>
1 parent 464b9e9 commit 3742f6c

File tree

6 files changed

+195
-10
lines changed

6 files changed

+195
-10
lines changed

src/components/layout/root/Root.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { BackToTopFAB, FABContainer } from '~/components/ui/fab'
2+
13
import { Content } from '../content/Content'
24
import { Footer } from '../footer'
35
import { Header } from '../header'
@@ -9,6 +11,9 @@ export const Root: Component = ({ children }) => {
911
<Content>{children}</Content>
1012

1113
<Footer />
14+
<FABContainer>
15+
<BackToTopFAB />
16+
</FABContainer>
1217
</>
1318
)
1419
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use client'
2+
3+
import { useViewport } from '~/atoms'
4+
import { usePageScrollLocationSelector } from '~/providers/root/page-scroll-info-provider'
5+
import { springScrollToTop } from '~/utils/spring'
6+
7+
import { FABBase } from './FABContainer'
8+
9+
export const BackToTopFAB = () => {
10+
const windowHeight = useViewport((v) => v.h)
11+
const shouldShow = usePageScrollLocationSelector(
12+
(scrollTop) => {
13+
return scrollTop > windowHeight / 5
14+
},
15+
[windowHeight],
16+
)
17+
18+
return (
19+
<FABBase
20+
id="to-top"
21+
aria-label="Back to top"
22+
show={shouldShow}
23+
onClick={springScrollToTop}
24+
>
25+
<i className="icon-[mingcute--arow-to-up-line]" />
26+
</FABBase>
27+
)
28+
}
+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use client'
2+
3+
import React, { useEffect, useState } from 'react'
4+
import type { PropsWithChildren } from 'react'
5+
6+
import { useStateToRef } from '~/hooks/common/use-state-ref'
7+
import { clsxm } from '~/utils/helper'
8+
9+
export interface FABConfig {
10+
id: string
11+
icon: JSX.Element
12+
onClick: () => void
13+
}
14+
15+
class FABStatic {
16+
private setState: React.Dispatch<React.SetStateAction<FABConfig[]>> | null =
17+
null
18+
register(setter: any) {
19+
this.setState = setter
20+
}
21+
destroy() {
22+
this.setState = null
23+
}
24+
25+
add(fabConfig: FABConfig) {
26+
if (!this.setState) return
27+
28+
const id = fabConfig.id
29+
30+
this.setState((state) => {
31+
if (state.find((config) => config.id === id)) return state
32+
return [...state, fabConfig]
33+
})
34+
35+
return () => {
36+
this.remove(fabConfig.id)
37+
}
38+
}
39+
40+
remove(id: string) {
41+
if (!this.setState) return
42+
this.setState((state) => {
43+
return state.filter((config) => config.id !== id)
44+
})
45+
}
46+
}
47+
48+
const fab = new FABStatic()
49+
50+
export const useFAB = (fabConfig: FABConfig) => {
51+
useEffect(() => {
52+
return fab.add(fabConfig)
53+
}, [])
54+
}
55+
56+
export const FABBase = (
57+
props: PropsWithChildren<
58+
{
59+
id: string
60+
show?: boolean
61+
children: JSX.Element
62+
} & React.DetailedHTMLProps<
63+
React.ButtonHTMLAttributes<HTMLButtonElement>,
64+
HTMLButtonElement
65+
>
66+
>,
67+
) => {
68+
const { children, show = true, ...extra } = props
69+
const { className, onTransitionEnd, ...rest } = extra
70+
71+
const [mounted, setMounted] = useState(true)
72+
const [appearTransition, setAppearTransition] = useState(false)
73+
const getMounted = useStateToRef(mounted)
74+
const handleTransitionEnd: React.TransitionEventHandler<HTMLButtonElement> = (
75+
e,
76+
) => {
77+
onTransitionEnd?.(e)
78+
79+
!show && setMounted(false)
80+
}
81+
82+
useEffect(() => {
83+
if (show && !getMounted.current) {
84+
setAppearTransition(true)
85+
setMounted(true)
86+
87+
requestAnimationFrame(() => {
88+
setAppearTransition(false)
89+
})
90+
}
91+
}, [show])
92+
93+
return (
94+
<button
95+
className={clsxm(
96+
'mt-2 inline-flex h-8 w-8 items-center justify-center rounded-md border border-accent bg-white text-accent opacity-50 transition-all duration-300 hover:opacity-100 focus:opacity-100 focus:outline-none',
97+
(!show || appearTransition) && 'translate-x-[60px]',
98+
!mounted && 'hidden',
99+
className,
100+
)}
101+
onTransitionEnd={handleTransitionEnd}
102+
{...rest}
103+
>
104+
{children}
105+
</button>
106+
)
107+
}
108+
109+
export const FABContainer = (props: {
110+
children: JSX.Element | JSX.Element[]
111+
}) => {
112+
const [fabConfig, setFabConfig] = useState<FABConfig[]>([])
113+
useEffect(() => {
114+
fab.register(setFabConfig)
115+
return () => {
116+
fab.destroy()
117+
}
118+
}, [])
119+
120+
const [serverSide, setServerSide] = useState(true)
121+
122+
useEffect(() => {
123+
setServerSide(false)
124+
}, [])
125+
126+
if (serverSide) return null
127+
128+
return (
129+
<div
130+
data-testid="fab-container"
131+
className={clsxm(
132+
'font-lg fixed bottom-4 bottom-[calc(1rem+env(safe-area-inset-bottom))] right-4 z-[9] flex flex-col',
133+
)}
134+
>
135+
{fabConfig.map((config) => {
136+
const { id, onClick, icon } = config
137+
return (
138+
<FABBase id={id} onClick={onClick} key={id}>
139+
{icon}
140+
</FABBase>
141+
)
142+
})}
143+
{props.children}
144+
</div>
145+
)
146+
}

src/components/ui/fab/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './BackToTopFAB'
2+
export * from './FABContainer'

src/components/ui/markdown/parsers/mention.tsx

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import type { MarkdownToJSX } from 'markdown-to-jsx'
2-
import { Priority, simpleInlineRegex } from 'markdown-to-jsx'
31
import React from 'react'
4-
import { CodiconGithubInverted, MdiTwitter, IcBaselineTelegram } from '~/components/icons/menu-collection'
2+
import { Priority, simpleInlineRegex } from 'markdown-to-jsx'
3+
import type { MarkdownToJSX } from 'markdown-to-jsx'
4+
5+
import {
6+
CodiconGithubInverted,
7+
IcBaselineTelegram,
8+
MdiTwitter,
9+
} from '~/components/icons/menu-collection'
510

6-
711
const prefixToIconMap = {
812
GH: <CodiconGithubInverted />,
913
TW: <MdiTwitter />,
@@ -42,17 +46,16 @@ export const MentionRule: MarkdownToJSX.Rule = {
4246
const { prefix, name } = content
4347
if (!name) {
4448
return null as any
45-
4649
}
4750

4851
// @ts-ignore
4952
const Icon = prefixToIconMap[prefix]
5053
// @ts-ignore
51-
const urlPrefix = prefixToUrlMap[prefix]
54+
const urlPrefix = prefixToUrlMap[prefix]
5255

5356
return (
54-
<div
55-
className="mr-2 inline-flex items-center space-x-2 align-bottom"
57+
<span
58+
className="mx-1 inline-flex items-center space-x-1 align-bottom"
5659
key={state?.key}
5760
>
5861
{Icon}
@@ -63,7 +66,7 @@ export const MentionRule: MarkdownToJSX.Rule = {
6366
>
6467
{name}
6568
</a>
66-
</div>
69+
</span>
6770
)
6871
},
6972
}

src/providers/root/page-scroll-info-provider.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ const usePageScrollLocation = () => useAtomValue(pageScrollLocationAtom)
4141
const usePageScrollDirection = () => useAtomValue(pageScrollDirectionAtom)
4242
const usePageScrollLocationSelector = <T,>(
4343
selector: (scrollY: number) => T,
44+
deps: any[] = [],
4445
): T =>
4546
useAtomValue(
4647
// @ts-ignore
4748
selectAtom(
4849
pageScrollLocationAtom,
49-
useCallback(($) => selector($), []),
50+
useCallback(($) => selector($), deps),
5051
),
5152
)
5253
export {

0 commit comments

Comments
 (0)