aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-02-28 03:57:02 +0000
committerClaude <noreply@anthropic.com>2026-02-28 03:57:02 +0000
commitf1a829f25540c4eff1b4b50548ba44518f8b231a (patch)
tree03e48387dc0139c77f534d729e7122f112ce879a
parent2802ccf8c1212e5ef49226e7063fbf008ea4c13e (diff)
downloadneko-f1a829f25540c4eff1b4b50548ba44518f8b231a.tar.gz
neko-f1a829f25540c4eff1b4b50548ba44518f8b231a.tar.bz2
neko-f1a829f25540c4eff1b4b50548ba44518f8b231a.zip
Fix feed-item overflow and flex shrink bug causing mobile horizontal scroll
The previous fix (overflow-x:hidden on .main-content and .item-description) was insufficient because mobile Safari ignores overflow-x:hidden on elements with overflow-y:auto. The real fix is item-level containment: - .feed-item: add overflow:hidden to create a BFC so no child content can push the viewport wider, even during async image load reflows - .item-title: add min-width:0 (fixes flex min-width:auto bug that prevents shrinking below content width) and overflow-wrap:break-word for long titles https://claude.ai/code/session_0141nhxmYfoFPVPZ813K1XFD
-rw-r--r--frontend-vanilla/src/mobile-overflow.test.ts121
-rw-r--r--frontend-vanilla/src/style.css3
2 files changed, 68 insertions, 56 deletions
diff --git a/frontend-vanilla/src/mobile-overflow.test.ts b/frontend-vanilla/src/mobile-overflow.test.ts
index 7463573..ec421d5 100644
--- a/frontend-vanilla/src/mobile-overflow.test.ts
+++ b/frontend-vanilla/src/mobile-overflow.test.ts
@@ -47,8 +47,6 @@ describe('Mobile horizontal overflow prevention', () => {
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
);
@@ -61,7 +59,6 @@ describe('Mobile horizontal overflow prevention', () => {
});
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%/
);
@@ -69,7 +66,6 @@ describe('Mobile horizontal overflow prevention', () => {
});
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%/
);
@@ -77,77 +73,90 @@ describe('Mobile horizontal overflow prevention', () => {
});
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();
+ it('.feed-item should have overflow hidden to contain all child content', () => {
+ // .feed-item must create a block formatting context so that
+ // no child (title, description, images) can push the viewport wider.
+ // This is critical on mobile where overflow-x:hidden on scrollable
+ // ancestors (.main-content with overflow-y:auto) is unreliable.
+ const feedItemBlock = cssContent.match(
+ /\.feed-item\s*\{[^}]*\}/
+ );
+ expect(feedItemBlock).not.toBeNull();
+ expect(feedItemBlock![0]).toMatch(/overflow:\s*hidden/);
+ });
- const desc = document.querySelector('.item-description');
- expect(desc).not.toBeNull();
- expect(desc!.innerHTML).toContain('<table');
+ it('.item-title should have min-width 0 to allow flex shrinking', () => {
+ // In a flex container, default min-width:auto prevents items from
+ // shrinking below their content width. Long titles push the layout
+ // wider than the viewport. min-width:0 fixes this.
+ const itemTitleBlock = cssContent.match(
+ /\.item-title\s*\{[^}]*\}/
+ );
+ expect(itemTitleBlock).not.toBeNull();
+ expect(itemTitleBlock![0]).toMatch(/min-width:\s*0/);
+ });
- // 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('.item-title should wrap long words', () => {
+ // Titles can contain long unbroken strings (URLs, technical terms).
+ // overflow-wrap ensures they wrap instead of overflowing.
+ const itemTitleBlock = cssContent.match(
+ /\.item-title\s*\{[^}]*\}/
+ );
+ expect(itemTitleBlock).not.toBeNull();
+ expect(itemTitleBlock![0]).toMatch(/overflow-wrap:\s*(break-word|anywhere)/);
});
+ });
- it('should render items with wide iframe content without breaking layout', () => {
+ describe('Rendered content containment after loadMore re-render', () => {
+ it('should contain items with long unbroken titles after re-render', () => {
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]);
+ const longTitle = 'A'.repeat(500); // simulate long unbroken title
+ const items = [
+ { _id: 1, title: 'Normal', url: 'http://example.com', publish_date: '2024-01-01', read: true, starred: false, description: '<p>first batch</p>' },
+ { _id: 2, title: longTitle, url: 'http://example.com', publish_date: '2024-01-01', read: false, starred: false, description: '<p>long title item</p>' },
+ ] as any;
+
+ // Initial render
+ store.setItems([items[0]]);
+ renderItems();
+
+ // Simulate loadMore re-render with appended items
+ store.setItems(items);
renderItems();
- const desc = document.querySelector('.item-description');
- expect(desc).not.toBeNull();
- expect(desc!.innerHTML).toContain('<iframe');
+ const feedItems = document.querySelectorAll('.feed-item');
+ expect(feedItems.length).toBe(2);
+ // The long title item should be contained within the layout
+ const longTitleEl = feedItems[1].querySelector('.item-title');
+ expect(longTitleEl).not.toBeNull();
+ expect(longTitleEl!.textContent!.trim().length).toBe(500);
});
- it('should render items with wide image using inline style without breaking layout', () => {
+ it('should contain items with wide description content after re-render', () => {
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]);
+ const items = [
+ { _id: 1, title: 'Item 1', url: 'http://example.com', publish_date: '2024-01-01', read: true, starred: false, description: '<p>ok</p>' },
+ { _id: 2, title: 'Item 2', url: 'http://example.com', publish_date: '2024-01-01', read: false, starred: false, description: '<table width="2000"><tr><td>wide</td></tr></table>' },
+ { _id: 3, title: 'Item 3', url: 'http://example.com', publish_date: '2024-01-01', read: false, starred: false, description: '<iframe width="1200" src="https://example.com"></iframe>' },
+ ] as any;
+
+ // Initial render
+ store.setItems([items[0]]);
+ renderItems();
+
+ // Simulate loadMore re-render
+ store.setItems(items);
renderItems();
- const desc = document.querySelector('.item-description');
- expect(desc).not.toBeNull();
- expect(desc!.innerHTML).toContain('<img');
+ const feedItems = document.querySelectorAll('.feed-item');
+ expect(feedItems.length).toBe(3);
});
});
});
diff --git a/frontend-vanilla/src/style.css b/frontend-vanilla/src/style.css
index 4d69390..47b2de2 100644
--- a/frontend-vanilla/src/style.css
+++ b/frontend-vanilla/src/style.css
@@ -452,6 +452,7 @@ select:focus {
border-bottom: none;
border-radius: 8px;
transition: background-color 0.2s ease, opacity 0.3s ease;
+ overflow: hidden;
}
@@ -471,6 +472,8 @@ select:focus {
color: var(--link-color);
display: block;
flex: 1;
+ min-width: 0;
+ overflow-wrap: break-word;
cursor: pointer;
}