feat: 添加预览效果页面并优化交互效果
refactor: 优化代码健壮性和类型安全 style: 更新字体样式和全局CSS fix: 修复IntersectionObserver潜在空引用问题 chore: 更新依赖和ESLint配置 build: 更新构建ID和路由配置
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
export function useFocusTrap<T extends HTMLElement>(isActive: boolean) {
|
||||
const containerRef = useRef<T>(null);
|
||||
const previousActiveElement = useRef<HTMLElement | null>(null);
|
||||
|
||||
const getFocusableElements = useCallback(() => {
|
||||
if (!containerRef.current) return [];
|
||||
|
||||
const elements = containerRef.current.querySelectorAll<HTMLElement>(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
return Array.from(elements).filter(
|
||||
(el) => !el.hasAttribute('disabled') && el.offsetParent !== null
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (!isActive || !containerRef.current) return;
|
||||
|
||||
if (event.key === 'Tab') {
|
||||
const focusableElements = getFocusableElements();
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (document.activeElement === firstElement) {
|
||||
event.preventDefault();
|
||||
lastElement?.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === lastElement) {
|
||||
event.preventDefault();
|
||||
firstElement?.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
previousActiveElement.current?.focus();
|
||||
}
|
||||
},
|
||||
[isActive, getFocusableElements]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) {
|
||||
previousActiveElement.current = document.activeElement as HTMLElement;
|
||||
|
||||
const focusableElements = getFocusableElements();
|
||||
focusableElements[0]?.focus();
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'unset';
|
||||
};
|
||||
}, [isActive, handleKeyDown, getFocusableElements]);
|
||||
|
||||
return containerRef;
|
||||
}
|
||||
@@ -19,13 +19,15 @@ export function useIntersectionObserver<T extends Element>(
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
if (!element) {return;}
|
||||
|
||||
if (freezeOnceVisible && isIntersecting) return;
|
||||
if (freezeOnceVisible && isIntersecting) {return;}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsIntersecting(entry.isIntersecting);
|
||||
if (entry) {
|
||||
setIsIntersecting(entry.isIntersecting);
|
||||
}
|
||||
},
|
||||
{ threshold, root, rootMargin }
|
||||
);
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
function getMediaQuerySnapshot(query: string): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
return window.matchMedia(query).matches;
|
||||
}
|
||||
|
||||
function getMediaQueryServerSnapshot(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function subscribeToMediaQuery(query: string, callback: () => void): () => void {
|
||||
if (typeof window === 'undefined') {
|
||||
return () => {};
|
||||
}
|
||||
const media = window.matchMedia(query);
|
||||
media.addEventListener('change', callback);
|
||||
return () => {
|
||||
media.removeEventListener('change', callback);
|
||||
};
|
||||
}
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches);
|
||||
}
|
||||
|
||||
const listener = (event: MediaQueryListEvent) => {
|
||||
setMatches(event.matches);
|
||||
};
|
||||
|
||||
media.addEventListener('change', listener);
|
||||
|
||||
return () => {
|
||||
media.removeEventListener('change', listener);
|
||||
};
|
||||
}, [matches, query]);
|
||||
|
||||
const matches = useSyncExternalStore(
|
||||
(callback) => subscribeToMediaQuery(query, callback),
|
||||
() => getMediaQuerySnapshot(query),
|
||||
getMediaQueryServerSnapshot
|
||||
);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ export function useScrollReveal({
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
if (!element) {return;}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
if (triggerOnce) {
|
||||
observer.unobserve(element);
|
||||
@@ -44,7 +44,7 @@ interface UseScrollProgressOptions {
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
export function useScrollProgress({ threshold = 0 }: UseScrollProgressOptions = {}) {
|
||||
export function useScrollProgress({ threshold: _threshold = 0 }: UseScrollProgressOptions = {}) {
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -74,13 +74,13 @@ export function useParallax({ speed = 0.5 }: UseParallaxOptions = {}) {
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!ref.current) return;
|
||||
|
||||
if (!ref.current) {return;}
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const scrolled = window.pageYOffset;
|
||||
const elementTop = rect.top + scrolled;
|
||||
const parallaxOffset = (scrolled - elementTop) * speed;
|
||||
|
||||
|
||||
setOffset(parallaxOffset);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user