Skip to content

Commit 10cf466

Browse files
committed
fix: toc scroller
Signed-off-by: Innei <[email protected]>
1 parent 7bf96c9 commit 10cf466

File tree

8 files changed

+58
-35
lines changed

8 files changed

+58
-35
lines changed

src/components/ui/transition/factor.tsx

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
'use client'
22

3-
import { memo, useMemo } from 'react'
3+
import { forwardRef, memo, useMemo } from 'react'
44
import { m } from 'framer-motion'
55
import type {
66
HTMLMotionProps,
77
Spring,
88
Target,
99
TargetAndTransition,
1010
} from 'framer-motion'
11-
import type { FC, PropsWithChildren } from 'react'
11+
import type {
12+
ForwardRefExoticComponent,
13+
PropsWithChildren,
14+
RefAttributes,
15+
} from 'react'
1216
import type { BaseTransitionProps } from './typings'
1317

1418
import { isHydrationEnded } from '~/components/common/HydrationEndDetector'
@@ -21,12 +25,13 @@ interface TransitionViewParams {
2125
preset?: Spring
2226
}
2327

24-
export const createTransitionView = (
25-
params: TransitionViewParams,
26-
): FC<PropsWithChildren<BaseTransitionProps>> => {
28+
export const createTransitionView = (params: TransitionViewParams) => {
2729
const { from, to, initial, preset } = params
2830

29-
const TransitionView = (props: PropsWithChildren<BaseTransitionProps>) => {
31+
const TransitionView = forwardRef<
32+
HTMLElement,
33+
PropsWithChildren<BaseTransitionProps>
34+
>((props, ref) => {
3035
const {
3136
timeout = {},
3237
duration = 0.5,
@@ -40,7 +45,9 @@ export const createTransitionView = (
4045

4146
const { enter = delay, exit = delay } = timeout
4247

43-
const MotionComponent = m[as] as FC<HTMLMotionProps<any>>
48+
const MotionComponent = m[as] as ForwardRefExoticComponent<
49+
HTMLMotionProps<any> & RefAttributes<HTMLElement>
50+
>
4451

4552
return (
4653
<MotionComponent
@@ -53,6 +60,7 @@ export const createTransitionView = (
5360
: initial || from,
5461
[],
5562
)}
63+
ref={ref}
5664
animate={{
5765
...to,
5866
transition: {
@@ -78,7 +86,8 @@ export const createTransitionView = (
7886
{props.children}
7987
</MotionComponent>
8088
)
81-
}
89+
})
90+
TransitionView.displayName = `forwardRef(TransitionView)`
8291
const MemoedTransitionView = memo(TransitionView)
8392
MemoedTransitionView.displayName = `MemoedTransitionView`
8493
return MemoedTransitionView

src/components/widgets/post/PostPinIcon.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use client'
22

3-
import { memo, useState } from 'react'
3+
import { useState } from 'react'
44

55
import { apiClient } from '~/lib/request'
66

77
import { PinIconToggle } from '../shared/PinIconToggle'
88

9-
export const PostPinIcon = memo(({ pin, id }: { pin: boolean; id: string }) => {
9+
export const PostPinIcon = ({ pin, id }: { pin: boolean; id: string }) => {
1010
const [pinState, setPinState] = useState(pin)
1111
return (
1212
<PinIconToggle
@@ -21,4 +21,4 @@ export const PostPinIcon = memo(({ pin, id }: { pin: boolean; id: string }) => {
2121
pin={pinState}
2222
/>
2323
)
24-
})
24+
}

src/components/widgets/shared/ActionAsideContainer.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ export const ActionAsideContainer: Component = ({ className, children }) => {
2828
return (
2929
<div
3030
className={clsxm(
31-
'absolute bottom-0 left-0 -mb-4 max-h-[300px] flex-col space-y-6 p-4 transition-all duration-200 ease-in-out',
31+
'absolute bottom-0 left-0 -mb-4 flex max-h-[300px] flex-col gap-6 p-4 transition-all duration-200 ease-in-out',
3232
!isEOA ? 'opacity-20 hover:opacity-100' : '',
3333
className,
34-
isEndOfPage && 'bottom-[100px]',
34+
isEndOfPage && 'bottom-[-30px] flex-row',
3535
)}
3636
>
3737
{children}

src/components/widgets/toc/TocAutoScroll.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useEffect } from 'react'
44

5-
import { escapeSelector } from './escapeSelector'
5+
import { escapeSelector } from '~/lib/dom'
66

77
export const TocAutoScroll: Component = () => {
88
useEffect(() => {

src/components/widgets/toc/TocTree.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,28 @@ const MemoedItem = memo<{
178178
// containerRef
179179
} = props
180180

181+
const itemRef = useRef<HTMLElement>(null)
182+
183+
useEffect(() => {
184+
if (!isActive) return
185+
186+
const $item = itemRef.current
187+
if (!$item) return
188+
const $container = $item.parentElement
189+
if (!$container) return
190+
const itemInContainerTop = $item.offsetTop
191+
const halfOfContainerHeight = $container.clientHeight / 2
192+
// 如果当前元素在容器的上半部分,不滚动
193+
if (itemInContainerTop < halfOfContainerHeight) {
194+
if ($container.scrollTop < halfOfContainerHeight) {
195+
$container.scrollTop = 0
196+
}
197+
} else {
198+
// 如果当前元素在容器的下半部分,滚动到容器中间
199+
$container.scrollTop = itemInContainerTop + halfOfContainerHeight
200+
}
201+
}, [isActive])
202+
181203
return (
182204
<RightToLeftTransitionView
183205
timeout={
@@ -189,6 +211,7 @@ const MemoedItem = memo<{
189211
key={heading.title}
190212
as="li"
191213
className="relative leading-none"
214+
ref={itemRef}
192215
>
193216
{isActive && (
194217
<m.span

src/components/widgets/toc/escapeSelector.tsx

-3
This file was deleted.

src/hooks/shared/use-mask-scrollarea.ts

+8-18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useViewport } from '~/atoms'
55

66
import { useEventCallback } from '../common/use-event-callback'
77

8+
const THRESHOLD = 0
89
export const useMaskScrollArea = <T extends HTMLElement = HTMLElement>() => {
910
const containerRef = useRef<T>(null)
1011
const [isScrollToBottom, setIsScrollToBottom] = useState(false)
@@ -17,7 +18,7 @@ export const useMaskScrollArea = <T extends HTMLElement = HTMLElement>() => {
1718

1819
if (!$) return
1920

20-
// if $ can not scroll, return null
21+
// if $ can not scroll
2122
if ($.scrollHeight <= $.clientHeight + 2) {
2223
setCanScroll(false)
2324
setIsScrollToBottom(false)
@@ -27,29 +28,18 @@ export const useMaskScrollArea = <T extends HTMLElement = HTMLElement>() => {
2728

2829
setCanScroll(true)
2930

30-
// to bottom
31-
if ($.scrollTop + $.clientHeight + 20 > $.scrollHeight) {
32-
setIsScrollToBottom(true)
33-
setIsScrollToTop(false)
34-
}
35-
36-
// if scroll to top,
37-
// set isScrollToTop to true
38-
else if ($.scrollTop < 20) {
39-
setIsScrollToTop(true)
40-
setIsScrollToBottom(false)
41-
} else {
42-
setIsScrollToBottom(false)
43-
setIsScrollToTop(false)
44-
}
31+
// if $ can scroll
32+
const isScrollToBottom =
33+
$.scrollTop + $.clientHeight >= $.scrollHeight - THRESHOLD
34+
const isScrollToTop = $.scrollTop <= THRESHOLD
35+
setIsScrollToBottom(isScrollToBottom)
36+
setIsScrollToTop(isScrollToTop)
4537
})
4638
useEffect(() => {
4739
const $ = containerRef.current
4840
if (!$) return
4941
$.addEventListener('scroll', eventHandler)
5042

51-
eventHandler()
52-
5343
return () => {
5444
$.removeEventListener('scroll', eventHandler)
5545
}

src/lib/dom.ts

+4
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ export const transitionViewIfSupported = (updateCb: () => any) => {
1616
updateCb()
1717
}
1818
}
19+
20+
export function escapeSelector(selector: string) {
21+
return selector.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&')
22+
}

0 commit comments

Comments
 (0)