fix(settings): simply setting modal scroll to section logic and highlight
- Renamed SettingSectionHighlightContext to SettingSectionHighlightIdContext for clarity. - Simplified highlight registration logic and improved scrolling behavior in SettingModalContent. - Enhanced visual feedback for highlighted sections with a new animation effect. - Cleaned up unused code and optimized imports for better maintainability. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
0661c5fc1e
commit
9f192f3cbf
|
|
@ -8,7 +8,6 @@ import {
|
|||
useDeferredValue,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
|
|
@ -19,7 +18,7 @@ import { ModalClose } from "~/components/ui/modal/stacked/components"
|
|||
import { SettingsTitle } from "~/modules/settings/title"
|
||||
|
||||
import { useAvailableSettings } from "../hooks/use-setting-ctx"
|
||||
import { SettingSectionHighlightContext } from "../section"
|
||||
import { SettingSectionHighlightIdContext } from "../section"
|
||||
import { getSettingPages } from "../settings-glob"
|
||||
import type { SettingPageConfig } from "../utils"
|
||||
import { SettingTabProvider, useSettingTab } from "./context"
|
||||
|
|
@ -46,83 +45,54 @@ export const SettingModalContent: FC<{
|
|||
}
|
||||
|
||||
const Content: FC<{
|
||||
initialSection?: string
|
||||
initialSection?: string | null
|
||||
}> = ({ initialSection }) => {
|
||||
const key = useDeferredValue(useSettingTab() || "general")
|
||||
const pages = getSettingPages()
|
||||
const { Component, loader } = pages[key]
|
||||
|
||||
const [scroller, setScroller] = useState<HTMLDivElement | null>(null)
|
||||
const sectionRefs = useRef(new Map<string, HTMLElement>())
|
||||
|
||||
const pendingSectionRef = useRef<string | null>(initialSection ?? null)
|
||||
const hasAppliedInitialSectionRef = useRef(false)
|
||||
const [highlightedSectionId, setHighlightedSectionId] = useState<string | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
pendingSectionRef.current = initialSection ?? null
|
||||
hasAppliedInitialSectionRef.current = false
|
||||
setHighlightedSectionId(undefined)
|
||||
}, [initialSection])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (scroller) {
|
||||
scroller.scrollTop = 0
|
||||
}
|
||||
}, [key])
|
||||
}, [key, scroller])
|
||||
|
||||
const scrollToSection = useCallback((sectionId: string) => {
|
||||
if (!sectionId) return false
|
||||
const element = sectionRefs.current.get(sectionId)
|
||||
if (!element) return false
|
||||
const scrollToSection = useCallback(
|
||||
(sectionId: string) => {
|
||||
if (!sectionId) return false
|
||||
if (!scroller) return false
|
||||
const element = scroller.querySelector(`[data-setting-section="${sectionId}"]`) as HTMLElement
|
||||
if (!element) return false
|
||||
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
})
|
||||
const elementTop = element.offsetTop
|
||||
const scrollerTop = scroller.scrollTop
|
||||
const delta = elementTop - scrollerTop
|
||||
|
||||
setHighlightedSectionId(sectionId)
|
||||
scroller.scrollTo({
|
||||
top: delta,
|
||||
behavior: "smooth",
|
||||
})
|
||||
|
||||
return true
|
||||
}, [])
|
||||
|
||||
const registerSection = useCallback(
|
||||
(sectionId: string, element: HTMLElement | null) => {
|
||||
if (!sectionId) return
|
||||
if (element) {
|
||||
sectionRefs.current.set(sectionId, element)
|
||||
if (pendingSectionRef.current === sectionId) {
|
||||
const handled = scrollToSection(sectionId)
|
||||
if (handled) {
|
||||
pendingSectionRef.current = null
|
||||
hasAppliedInitialSectionRef.current = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sectionRefs.current.delete(sectionId)
|
||||
}
|
||||
return true
|
||||
},
|
||||
[scrollToSection],
|
||||
[scroller],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialSection || hasAppliedInitialSectionRef.current) return
|
||||
|
||||
const handled = scrollToSection(initialSection)
|
||||
if (handled) {
|
||||
hasAppliedInitialSectionRef.current = true
|
||||
pendingSectionRef.current = null
|
||||
} else {
|
||||
pendingSectionRef.current = initialSection
|
||||
if (initialSection) {
|
||||
scrollToSection(initialSection)
|
||||
}
|
||||
}, [initialSection, key, scrollToSection])
|
||||
|
||||
const highlightContextValue = useMemo(
|
||||
() => ({
|
||||
highlightedSectionId: highlightedSectionId ?? undefined,
|
||||
registerSection,
|
||||
}),
|
||||
[highlightedSectionId, registerSection],
|
||||
)
|
||||
}, [initialSection, scrollToSection])
|
||||
|
||||
const config = (useLoaderData() || loader || {}) as SettingPageConfig
|
||||
if (!Component) return null
|
||||
|
|
@ -140,9 +110,9 @@ const Content: FC<{
|
|||
config.viewportClassName,
|
||||
)}
|
||||
>
|
||||
<SettingSectionHighlightContext value={highlightContextValue}>
|
||||
<SettingSectionHighlightIdContext value={initialSection!}>
|
||||
<Component />
|
||||
</SettingSectionHighlightContext>
|
||||
</SettingSectionHighlightIdContext>
|
||||
|
||||
<div className="h-16" />
|
||||
<p className="absolute inset-x-0 bottom-4 flex items-center justify-center gap-1 text-xs opacity-80">
|
||||
|
|
|
|||
|
|
@ -9,13 +9,7 @@ import { titleCase } from "title-case"
|
|||
|
||||
import { SettingActionItem, SettingDescription, SettingSwitch } from "./control"
|
||||
|
||||
type SettingSectionHighlightContextValue = {
|
||||
highlightedSectionId?: string
|
||||
registerSection: (sectionId: string, element: HTMLElement | null) => void
|
||||
}
|
||||
|
||||
export const SettingSectionHighlightContext =
|
||||
createContext<SettingSectionHighlightContextValue | null>(null)
|
||||
export const SettingSectionHighlightIdContext = createContext<string | null>(null)
|
||||
|
||||
export const SettingSectionTitle: FC<{
|
||||
title: string | ReactNode
|
||||
|
|
@ -23,32 +17,59 @@ export const SettingSectionTitle: FC<{
|
|||
margin?: "compact" | "normal"
|
||||
sectionId?: string
|
||||
}> = ({ title, margin, className, sectionId }) => {
|
||||
const highlightCtx = use(SettingSectionHighlightContext)
|
||||
const highlightedSectionId = use(SettingSectionHighlightIdContext)
|
||||
const elementRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const isHighlighted = !!sectionId && highlightedSectionId === sectionId && !!elementRef.current
|
||||
|
||||
useEffect(() => {
|
||||
if (!sectionId || !highlightCtx) return
|
||||
highlightCtx.registerSection(sectionId, elementRef.current)
|
||||
return () => {
|
||||
highlightCtx.registerSection(sectionId, null)
|
||||
if (!isHighlighted) {
|
||||
return
|
||||
}
|
||||
}, [highlightCtx, sectionId])
|
||||
|
||||
const isHighlighted =
|
||||
!!sectionId && highlightCtx?.highlightedSectionId === sectionId && !!elementRef.current
|
||||
let rollingAnimation: Animation | null = null
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const highlightedElement = elementRef.current?.querySelector(
|
||||
"[data-highlighted-element]",
|
||||
) as HTMLElement
|
||||
if (!highlightedElement) {
|
||||
clearTimeout(timer)
|
||||
return
|
||||
}
|
||||
const keyframeEffect = new KeyframeEffect(
|
||||
highlightedElement,
|
||||
[
|
||||
{
|
||||
backgroundColor: "color-mix(in srgb, hsl(var(--fo-a)) 33%, hsl(var(--background)) 67%)",
|
||||
},
|
||||
{ backgroundColor: "transparent" },
|
||||
],
|
||||
{
|
||||
duration: 1000,
|
||||
easing: "ease-in-out",
|
||||
},
|
||||
)
|
||||
rollingAnimation = new Animation(keyframeEffect, document.timeline)
|
||||
rollingAnimation.play()
|
||||
}, 500)
|
||||
return () => {
|
||||
rollingAnimation?.cancel()
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [isHighlighted])
|
||||
return (
|
||||
<div
|
||||
ref={elementRef}
|
||||
data-setting-section={sectionId}
|
||||
data-highlighted={isHighlighted ? "true" : undefined}
|
||||
className={cn(
|
||||
"shrink-0 text-headline font-bold text-text opacity-50 transition-colors duration-300 first:mt-0",
|
||||
"relative shrink-0 text-headline font-bold text-text/80 first:mt-0",
|
||||
margin === "compact" ? "mb-2 mt-8" : "mb-4 mt-10",
|
||||
isHighlighted && "-ml-3 rounded-lg border border-folo px-3 py-1.5 opacity-100",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isHighlighted && <div className="absolute -inset-4 rounded-lg" data-highlighted-element />}
|
||||
{typeof title === "string" ? titleCase(title) : title}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export const TaskSchedulingSection = () => {
|
|||
size={"sm"}
|
||||
variant={"outline"}
|
||||
onClick={handleCreateTask}
|
||||
buttonClassName="!-translate-y-7"
|
||||
>
|
||||
<i className="i-mgc-add-cute-re mr-2 size-4" />
|
||||
{t("tasks.actions.new_task")}
|
||||
|
|
|
|||
Loading…
Reference in New Issue