diff options
| author | Claude <noreply@anthropic.com> | 2026-02-27 15:38:08 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-02-27 15:38:08 +0000 |
| commit | 2802ccf8c1212e5ef49226e7063fbf008ea4c13e (patch) | |
| tree | bed0c04c4a7b89e929c1b0ca487eeab689515eb8 | |
| parent | fea87c9814618646489d8403d584da25a38e6ba7 (diff) | |
| download | neko-2802ccf8c1212e5ef49226e7063fbf008ea4c13e.tar.gz neko-2802ccf8c1212e5ef49226e7063fbf008ea4c13e.tar.bz2 neko-2802ccf8c1212e5ef49226e7063fbf008ea4c13e.zip | |
Fix mobile horizontal scrolling caused by wide RSS content
RSS feeds can contain tables, iframes, and other elements with explicit
widths that overflow the viewport on mobile. Added overflow-x: hidden to
.item-description and .main-content, and extended max-width: 100% to
cover table and iframe elements inside item descriptions.
Includes TDD tests verifying the CSS containment rules.
https://claude.ai/code/session_0141nhxmYfoFPVPZ813K1XFD
| -rw-r--r-- | frontend-vanilla/src/mobile-overflow.test.ts | 153 | ||||
| -rw-r--r-- | frontend-vanilla/src/style.css | 6 |
2 files changed, 158 insertions, 1 deletions
diff --git a/frontend-vanilla/src/mobile-overflow.test.ts b/frontend-vanilla/src/mobile-overflow.test.ts new file mode 100644 index 0000000..7463573 --- /dev/null +++ b/frontend-vanilla/src/mobile-overflow.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { store } from './store'; +import { renderLayout, renderItems } from './main'; +import { apiFetch } from './api'; + +// Mock api +vi.mock('./api', () => ({ + apiFetch: vi.fn() +})); + +// Mock IntersectionObserver +class MockIntersectionObserver { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); +} +vi.stubGlobal('IntersectionObserver', MockIntersectionObserver); + +// Read the main stylesheet once for CSS rule assertions +const cssContent = readFileSync(resolve(__dirname, 'style.css'), 'utf-8'); + +describe('Mobile horizontal overflow prevention', () => { + beforeEach(() => { + document.body.innerHTML = '<div id="app"></div>'; + vi.stubGlobal('location', { + href: 'http://localhost/v3/', + pathname: '/v3/', + search: '', + assign: vi.fn(), + replace: vi.fn() + }); + vi.stubGlobal('history', { pushState: vi.fn() }); + Element.prototype.scrollIntoView = vi.fn(); + vi.clearAllMocks(); + store.setFeeds([]); + store.setTags([]); + store.setItems([]); + + vi.mocked(apiFetch).mockResolvedValue({ + ok: true, + status: 200, + json: async () => [] + } as Response); + }); + + describe('CSS containment rules', () => { + it('.item-description should have overflow-x hidden to contain wide RSS content', () => { + // .item-description must prevent wide child elements (tables, iframes) + // from causing horizontal viewport overflow + const itemDescBlock = cssContent.match( + /\.item-description\s*\{[^}]*\}/g + ); + expect(itemDescBlock).not.toBeNull(); + const mainBlock = itemDescBlock!.find( + block => !block.includes('img') && !block.includes('video') && !block.includes('pre') && !block.includes(' a') + ); + expect(mainBlock).toBeDefined(); + expect(mainBlock).toMatch(/overflow-x:\s*hidden/); + }); + + it('.item-description should constrain tables with max-width', () => { + // RSS feeds commonly contain <table> elements with explicit widths + const tableRule = cssContent.match( + /\.item-description\s+table[^{]*\{[^}]*max-width:\s*100%/ + ); + expect(tableRule).not.toBeNull(); + }); + + it('.item-description should constrain iframes with max-width', () => { + // RSS feeds commonly embed iframes (YouTube, etc.) with fixed widths + const iframeRule = cssContent.match( + /\.item-description\s+iframe[^{]*\{[^}]*max-width:\s*100%/ + ); + expect(iframeRule).not.toBeNull(); + }); + + it('.main-content should explicitly set overflow-x hidden', () => { + // .main-content must not allow horizontal scrolling + const mainContentBlock = cssContent.match( + /\.main-content\s*\{[^}]*\}/ + ); + expect(mainContentBlock).not.toBeNull(); + expect(mainContentBlock![0]).toMatch(/overflow-x:\s*hidden/); + }); + }); + + describe('Rendered content containment', () => { + it('should render items with wide table content without breaking layout', () => { + renderLayout(); + const wideTableItem = { + _id: 1, + title: 'Wide Table Post', + url: 'http://example.com', + publish_date: '2024-01-01', + read: false, + starred: false, + description: '<table width="2000"><tr><td>Very wide table from RSS</td></tr></table>' + } as any; + store.setItems([wideTableItem]); + renderItems(); + + const desc = document.querySelector('.item-description'); + expect(desc).not.toBeNull(); + expect(desc!.innerHTML).toContain('<table'); + + // The item-description element should be inside main-content + // which constrains overflow + const mainContent = document.getElementById('main-content'); + expect(mainContent).not.toBeNull(); + expect(mainContent!.contains(desc!)).toBe(true); + }); + + it('should render items with wide iframe content without breaking layout', () => { + renderLayout(); + const wideIframeItem = { + _id: 2, + title: 'Embedded Video Post', + url: 'http://example.com', + publish_date: '2024-01-01', + read: false, + starred: false, + description: '<iframe width="1200" height="600" src="https://example.com/embed"></iframe>' + } as any; + store.setItems([wideIframeItem]); + renderItems(); + + const desc = document.querySelector('.item-description'); + expect(desc).not.toBeNull(); + expect(desc!.innerHTML).toContain('<iframe'); + }); + + it('should render items with wide image using inline style without breaking layout', () => { + renderLayout(); + const wideImgItem = { + _id: 3, + title: 'Wide Image Post', + url: 'http://example.com', + publish_date: '2024-01-01', + read: false, + starred: false, + description: '<img style="width: 1500px" src="https://example.com/wide.jpg">' + } as any; + store.setItems([wideImgItem]); + renderItems(); + + const desc = document.querySelector('.item-description'); + expect(desc).not.toBeNull(); + expect(desc!.innerHTML).toContain('<img'); + }); + }); +}); diff --git a/frontend-vanilla/src/style.css b/frontend-vanilla/src/style.css index 27a8d28..4d69390 100644 --- a/frontend-vanilla/src/style.css +++ b/frontend-vanilla/src/style.css @@ -288,6 +288,7 @@ html { width: 100%; height: 100%; min-width: 0; + overflow-x: hidden; overflow-y: auto; background-color: var(--bg-color); padding: 1.5rem 2rem; @@ -517,6 +518,7 @@ select:focus { margin-top: 1rem; overflow-wrap: break-word; word-break: break-word; + overflow-x: hidden; } .item-description a { @@ -526,7 +528,9 @@ select:focus { .item-description img, .item-description video, -.item-description pre { +.item-description pre, +.item-description table, +.item-description iframe { max-width: 100%; height: auto; display: block; |
