In scrolled mode the notch-area masks the top safe-area inset with opaque bg-base-100 so content scrolling under the status bar is hidden, but it painted over the background texture (.foliate-viewer::before at the z-0 layer), leaving a flat untextured strip across the unsafe header area. Give the mask its own texture ::before (.notch-masked in textures.ts) and make the element span the grid cell, clipped down to the inset strip with clip-path — background-size cover/contain resolves against the element box, so the full-cell box is what keeps the mask's tiles aligned with the viewer's at the seam. clip-path also clips hit-testing, so the click target stays the inset strip only. Verified on a Xiaomi 13: the strip now renders the texture with a pixel-continuous seam (row-to-row MAE at the boundary dropped from 11913 to 230, the level of ordinary texture rows), and elementsFromPoint confirms the notch is hit-testable only inside the strip. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import SectionInfo from '@/app/reader/components/SectionInfo';
|
||||
|
||||
vi.mock('@/context/EnvContext', () => ({
|
||||
useEnv: () => ({ appService: { isAndroidApp: false, isMobile: true } }),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/themeStore', () => ({
|
||||
useThemeStore: () => ({ systemUIVisible: false, statusBarHeight: 0 }),
|
||||
}));
|
||||
|
||||
vi.mock('@/store/readerStore', () => ({
|
||||
useReaderStore: () => ({
|
||||
hoveredBookKey: '',
|
||||
getView: () => null,
|
||||
getViewSettings: () => ({ marginTopPx: 44 }),
|
||||
setHoveredBookKey: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useTranslation', () => ({
|
||||
useTranslation: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
const baseProps = {
|
||||
bookKey: 'book-1',
|
||||
section: 'Chapter 1',
|
||||
showDoubleBorder: false,
|
||||
isScrolled: true,
|
||||
isVertical: false,
|
||||
isEink: false,
|
||||
horizontalGap: 5,
|
||||
contentInsets: { top: 89, right: 0, bottom: 0, left: 0 },
|
||||
gridInsets: { top: 45, right: 0, bottom: 0, left: 0 },
|
||||
};
|
||||
|
||||
describe('SectionInfo notch mask', () => {
|
||||
it('spans the grid cell and clips to the top inset so its texture aligns with the viewer (#4486)', () => {
|
||||
// The scrolled-mode notch mask hides content scrolling under the status bar,
|
||||
// but must not occlude the background texture. Its texture ::before only
|
||||
// tile-aligns with .foliate-viewer::before (background-size cover/contain
|
||||
// resolves against the element box) if the notch shares the viewer's paint
|
||||
// box: full grid cell, with the visible/hit area clipped to the inset strip.
|
||||
const { container } = render(<SectionInfo {...baseProps} />);
|
||||
const notch = container.querySelector('.notch-area') as HTMLElement;
|
||||
|
||||
expect(notch).not.toBeNull();
|
||||
expect(notch.classList.contains('inset-0')).toBe(true);
|
||||
expect(notch.classList.contains('notch-masked')).toBe(true);
|
||||
expect(notch.classList.contains('bg-base-100')).toBe(true);
|
||||
expect(notch.style.clipPath).toBe('inset(0 0 calc(100% - 45px) 0)');
|
||||
});
|
||||
|
||||
it('keeps the notch transparent and untextured in paginated mode', () => {
|
||||
const { container } = render(<SectionInfo {...baseProps} isScrolled={false} />);
|
||||
const notch = container.querySelector('.notch-area') as HTMLElement;
|
||||
|
||||
expect(notch.classList.contains('notch-masked')).toBe(false);
|
||||
expect(notch.classList.contains('bg-base-100')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the notch transparent and untextured in vertical scrolled mode', () => {
|
||||
const { container } = render(<SectionInfo {...baseProps} isVertical={true} />);
|
||||
const notch = container.querySelector('.notch-area') as HTMLElement;
|
||||
|
||||
expect(notch.classList.contains('notch-masked')).toBe(false);
|
||||
expect(notch.classList.contains('bg-base-100')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { mountBackgroundTexture, unmountBackgroundTexture } from '@/styles/textures';
|
||||
|
||||
afterEach(() => {
|
||||
unmountBackgroundTexture(document);
|
||||
});
|
||||
|
||||
describe('mountBackgroundTexture', () => {
|
||||
it('covers the scrolled-mode notch mask so the top inset strip is textured (#4486)', () => {
|
||||
mountBackgroundTexture(document, {
|
||||
id: 'paper',
|
||||
name: 'Paper',
|
||||
url: '/images/paper-texture.png',
|
||||
});
|
||||
|
||||
const style = document.getElementById('background-texture');
|
||||
expect(style?.textContent).toContain('.notch-masked::before');
|
||||
});
|
||||
});
|
||||
@@ -56,14 +56,20 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
'notch-area absolute left-0 right-0 top-0 z-10',
|
||||
isScrolled && !isVertical && 'bg-base-100',
|
||||
// Spans the grid cell and clips down to the top inset strip so the
|
||||
// texture ::before (.notch-masked, see styles/textures.ts) shares
|
||||
// .foliate-viewer::before's paint box — background-size cover/contain
|
||||
// resolves against the element box, so a strip-sized box would
|
||||
// mis-tile at the seam (#4486). clip-path also clips hit-testing,
|
||||
// keeping the click target the inset strip only.
|
||||
'notch-area absolute inset-0 z-10',
|
||||
isScrolled && !isVertical && 'notch-masked bg-base-100',
|
||||
)}
|
||||
role='none'
|
||||
tabIndex={-1}
|
||||
onClick={handleNotchClick}
|
||||
style={{
|
||||
height: `${topInset}px`,
|
||||
clipPath: `inset(0 0 calc(100% - ${topInset}px) 0)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -94,7 +94,7 @@ const createTextureCSS = (texture: BackgroundTexture) => {
|
||||
}
|
||||
|
||||
body::before, .sidebar-container::before, .notebook-container::before,
|
||||
.foliate-viewer::before {
|
||||
.foliate-viewer::before, .notch-masked::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
Reference in New Issue
Block a user