mirror of https://github.com/djteang/OrangeTV.git
Fix stale overlay scroll locks
This commit is contained in:
parent
f8eb7cea4c
commit
12d88d3991
File diff suppressed because one or more lines are too long
|
|
@ -25,13 +25,13 @@ import {
|
|||
import { getDoubanCategories } from '@/lib/douban.client';
|
||||
import { DoubanItem } from '@/lib/types';
|
||||
|
||||
import { AnnouncementBanner } from '@/components/AnnouncementBanner';
|
||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||
import ContinueWatching from '@/components/ContinueWatching';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import ScrollableRow from '@/components/ScrollableRow';
|
||||
import { useSite } from '@/components/SiteProvider';
|
||||
import VideoCard from '@/components/VideoCard';
|
||||
import { AppDialog } from '@/components/ui/HeroPrimitives';
|
||||
|
||||
function HomeClient() {
|
||||
const [activeTab, setActiveTab] = useState<'home' | 'favorites'>('home');
|
||||
|
|
@ -190,6 +190,13 @@ function HomeClient() {
|
|||
</div>
|
||||
|
||||
<div className='mx-auto max-w-[1380px] space-y-10'>
|
||||
{announcement && showAnnouncement && (
|
||||
<AnnouncementBanner
|
||||
announcement={announcement}
|
||||
onDismiss={() => handleCloseAnnouncement(announcement)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'favorites' ? (
|
||||
// 收藏夹视图
|
||||
<Card>
|
||||
|
|
@ -427,25 +434,6 @@ function HomeClient() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{announcement && (
|
||||
<AppDialog
|
||||
isOpen={showAnnouncement}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) handleCloseAnnouncement(announcement);
|
||||
}}
|
||||
title='提示'
|
||||
footer={
|
||||
<Button
|
||||
fullWidth
|
||||
onPress={() => handleCloseAnnouncement(announcement)}
|
||||
>
|
||||
我知道了
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p className='text-sm leading-6 text-muted'>{announcement}</p>
|
||||
</AppDialog>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import { Button, Card } from '@heroui/react';
|
||||
|
||||
interface AnnouncementBannerProps {
|
||||
announcement: string;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export function AnnouncementBanner({
|
||||
announcement,
|
||||
onDismiss,
|
||||
}: AnnouncementBannerProps) {
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header className='flex-row items-start justify-between gap-4'>
|
||||
<div>
|
||||
<Card.Description>Announcement</Card.Description>
|
||||
<Card.Title>提示</Card.Title>
|
||||
</div>
|
||||
<Button size='sm' variant='secondary' onPress={onDismiss}>
|
||||
我知道了
|
||||
</Button>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p className='text-sm leading-6 text-muted'>{announcement}</p>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -72,29 +72,6 @@ export const UserMenu: React.FC = () => {
|
|||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const [showCropper, setShowCropper] = useState(false);
|
||||
|
||||
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
||||
useEffect(() => {
|
||||
if (isSettingsOpen || isChangePasswordOpen || isChangeAvatarOpen) {
|
||||
const body = document.body;
|
||||
const html = document.documentElement;
|
||||
|
||||
// 保存原始样式
|
||||
const originalBodyOverflow = body.style.overflow;
|
||||
const originalHtmlOverflow = html.style.overflow;
|
||||
|
||||
// 只设置 overflow 来阻止滚动
|
||||
body.style.overflow = 'hidden';
|
||||
html.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
|
||||
// 恢复所有原始样式
|
||||
body.style.overflow = originalBodyOverflow;
|
||||
html.style.overflow = originalHtmlOverflow;
|
||||
};
|
||||
}
|
||||
}, [isSettingsOpen, isChangePasswordOpen]);
|
||||
|
||||
// 设置相关状态
|
||||
const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true);
|
||||
const [doubanProxyUrl, setDoubanProxyUrl] = useState('');
|
||||
|
|
|
|||
|
|
@ -49,28 +49,6 @@ export const VersionPanel: React.FC<VersionPanelProps> = ({
|
|||
return () => setMounted(false);
|
||||
}, []);
|
||||
|
||||
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const body = document.body;
|
||||
const html = document.documentElement;
|
||||
|
||||
// 保存原始样式
|
||||
const originalBodyOverflow = body.style.overflow;
|
||||
const originalHtmlOverflow = html.style.overflow;
|
||||
|
||||
// 只设置 overflow 来阻止滚动
|
||||
body.style.overflow = 'hidden';
|
||||
html.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
// 恢复所有原始样式
|
||||
body.style.overflow = originalBodyOverflow;
|
||||
html.style.overflow = originalHtmlOverflow;
|
||||
};
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// 获取远程变更日志
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { AnnouncementBanner } from '../AnnouncementBanner';
|
||||
|
||||
describe('AnnouncementBanner', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
document.documentElement.style.overflow = '';
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
it('renders a non-blocking announcement without a dialog or scroll lock', () => {
|
||||
const onDismiss = jest.fn();
|
||||
|
||||
render(
|
||||
<AnnouncementBanner
|
||||
announcement='请注意站点公告'
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('请注意站点公告')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(document.documentElement.style.overflow).toBe('');
|
||||
expect(document.body.style.overflow).toBe('');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '我知道了' }));
|
||||
|
||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -20,11 +20,41 @@ import type {
|
|||
SpinnerProps,
|
||||
TabsProps,
|
||||
} from '@heroui/react';
|
||||
import { forwardRef } from 'react';
|
||||
import { forwardRef, useEffect } from 'react';
|
||||
import type { Key, ReactNode } from 'react';
|
||||
|
||||
type AppButtonProps = ButtonProps;
|
||||
|
||||
let activeRootOverlayCount = 0;
|
||||
|
||||
function releaseRootScrollLockIfIdle() {
|
||||
if (activeRootOverlayCount > 0 || typeof document === 'undefined') return;
|
||||
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
|
||||
if (html.style.overflow === 'hidden') {
|
||||
html.style.overflow = '';
|
||||
}
|
||||
|
||||
if (body.style.overflow === 'hidden') {
|
||||
body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
function useRootOverlayCleanup(isOpen: boolean) {
|
||||
useEffect(() => {
|
||||
if (!isOpen) return undefined;
|
||||
|
||||
activeRootOverlayCount += 1;
|
||||
|
||||
return () => {
|
||||
activeRootOverlayCount = Math.max(0, activeRootOverlayCount - 1);
|
||||
releaseRootScrollLockIfIdle();
|
||||
};
|
||||
}, [isOpen]);
|
||||
}
|
||||
|
||||
export const AppButton = forwardRef<HTMLButtonElement, AppButtonProps>(
|
||||
function AppButton(props, ref) {
|
||||
return <Button ref={ref} {...props} />;
|
||||
|
|
@ -151,6 +181,7 @@ export function AppDialog({
|
|||
isDismissable = true,
|
||||
}: AppDialogProps) {
|
||||
const state = useOverlayState({ isOpen, onOpenChange });
|
||||
useRootOverlayCleanup(isOpen);
|
||||
|
||||
return (
|
||||
<Modal state={state}>
|
||||
|
|
@ -202,6 +233,7 @@ export function AppDrawer({
|
|||
isDismissable = true,
|
||||
}: AppDrawerProps) {
|
||||
const state = useOverlayState({ isOpen, onOpenChange });
|
||||
useRootOverlayCleanup(isOpen);
|
||||
|
||||
return (
|
||||
<Drawer state={state}>
|
||||
|
|
|
|||
|
|
@ -72,4 +72,38 @@ describe('HeroPrimitives', () => {
|
|||
fireEvent.click(screen.getByRole('button', { name: '播放' }));
|
||||
expect(onOpenChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('clears stale root scroll locks when dialogs unmount', () => {
|
||||
const onOpenChange = jest.fn();
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
const { unmount } = render(
|
||||
<AppDialog isOpen title='提示' onOpenChange={onOpenChange}>
|
||||
<p>内容</p>
|
||||
</AppDialog>
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(document.documentElement.style.overflow).toBe('');
|
||||
expect(document.body.style.overflow).toBe('');
|
||||
});
|
||||
|
||||
it('clears stale root scroll locks when drawers unmount', () => {
|
||||
const onOpenChange = jest.fn();
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
const { unmount } = render(
|
||||
<AppDrawer isOpen title='更多操作' onOpenChange={onOpenChange}>
|
||||
<p>内容</p>
|
||||
</AppDrawer>
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(document.documentElement.style.overflow).toBe('');
|
||||
expect(document.body.style.overflow).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue