From e4919af3f03afb11b38de6da278c9b05b2349211 Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Mon, 4 May 2026 15:03:01 -0600 Subject: [PATCH 1/5] fix(segment-view): prevent NaN/Infinity scrollRatio when a single content item is present --- core/src/components/segment-view/segment-view.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/components/segment-view/segment-view.tsx b/core/src/components/segment-view/segment-view.tsx index 4023d46971c..298ac772086 100644 --- a/core/src/components/segment-view/segment-view.tsx +++ b/core/src/components/segment-view/segment-view.tsx @@ -47,6 +47,8 @@ export class SegmentView implements ComponentInterface { handleScroll(ev: Event) { const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement; const max = scrollWidth - clientWidth; + // When only one content item is present max is 0 — skip to avoid NaN/Infinity scrollRatio. + if (max <= 0) return; const scrollRatio = (isRTL(this.el) ? -1 : 1) * (scrollLeft / max); this.ionSegmentViewScroll.emit({ From b761da8caa7f773604389e4335fed987a50cd65a Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Wed, 6 May 2026 10:53:27 -0600 Subject: [PATCH 2/5] test(segment-view): add regression test for single content NaN scrollRatio --- .../test/basic/segment-view.e2e.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/src/components/segment-view/test/basic/segment-view.e2e.ts b/core/src/components/segment-view/test/basic/segment-view.e2e.ts index f3f35626eaa..2460d525e4e 100644 --- a/core/src/components/segment-view/test/basic/segment-view.e2e.ts +++ b/core/src/components/segment-view/test/basic/segment-view.e2e.ts @@ -128,6 +128,50 @@ configs({ modes: ['md'] }).forEach(({ title, config }) => { await expect(segmentButton).toHaveClass(/segment-button-checked/); }); + test('should not emit ionSegmentViewScroll with NaN or Infinity scrollRatio when only one content item is present', async ({ + page, + }) => { + await page.setContent( + ` + + + Only + + + + Only + + `, + config + ); + + const scrollRatios: number[] = []; + + await page.exposeFunction('recordScrollRatio', (ratio: number) => { + scrollRatios.push(ratio); + }); + + await page.evaluate(() => { + document.querySelector('ion-segment-view')!.addEventListener('ionSegmentViewScroll', (ev: any) => { + (window as any).recordScrollRatio(ev.detail.scrollRatio); + }); + }); + + // Programmatically dispatch a scroll event on the segment-view host element + // to simulate what the browser fires when scrollLeft changes. + await page.locator('ion-segment-view').evaluate((el: HTMLElement) => { + el.dispatchEvent(new Event('scroll', { bubbles: true })); + }); + + await page.waitForTimeout(50); + + // ionSegmentViewScroll should not have fired at all (max === 0 guard), + // but if it did fire for any reason the scrollRatio must be finite. + for (const ratio of scrollRatios) { + expect(isFinite(ratio)).toBe(true); + } + }); + test('should set correct segment button as checked and show correct content when programmatically setting the segment value', async ({ page, }) => { From dcbf3917ffad61db62d4d9486bb58d43a659aa7e Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Wed, 6 May 2026 10:54:38 -0600 Subject: [PATCH 3/5] fix(segment-view): reset scroll-end timer on early return for single content --- core/src/components/segment-view/segment-view.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/components/segment-view/segment-view.tsx b/core/src/components/segment-view/segment-view.tsx index 298ac772086..73dc8a04cc0 100644 --- a/core/src/components/segment-view/segment-view.tsx +++ b/core/src/components/segment-view/segment-view.tsx @@ -48,7 +48,12 @@ export class SegmentView implements ComponentInterface { const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement; const max = scrollWidth - clientWidth; // When only one content item is present max is 0 — skip to avoid NaN/Infinity scrollRatio. - if (max <= 0) return; + // Still reset the timeout so isManualScroll isn't cleared prematurely if setContent + // started the timer and a stray scroll event arrives on a non-overflowing element. + if (max <= 0) { + this.resetScrollEndTimeout(); + return; + } const scrollRatio = (isRTL(this.el) ? -1 : 1) * (scrollLeft / max); this.ionSegmentViewScroll.emit({ From 0302f23e659eb2f2df2d11b869a64f47e8255b1b Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Wed, 6 May 2026 11:19:52 -0600 Subject: [PATCH 4/5] test(segment-view): assert ionSegmentViewScroll does not fire with single content item --- .../test/basic/segment-view.e2e.ts | 21 ++++--------------- core/src/components/segment/segment.tsx | 1 + 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/core/src/components/segment-view/test/basic/segment-view.e2e.ts b/core/src/components/segment-view/test/basic/segment-view.e2e.ts index 2460d525e4e..a7f3afe4493 100644 --- a/core/src/components/segment-view/test/basic/segment-view.e2e.ts +++ b/core/src/components/segment-view/test/basic/segment-view.e2e.ts @@ -145,17 +145,7 @@ configs({ modes: ['md'] }).forEach(({ title, config }) => { config ); - const scrollRatios: number[] = []; - - await page.exposeFunction('recordScrollRatio', (ratio: number) => { - scrollRatios.push(ratio); - }); - - await page.evaluate(() => { - document.querySelector('ion-segment-view')!.addEventListener('ionSegmentViewScroll', (ev: any) => { - (window as any).recordScrollRatio(ev.detail.scrollRatio); - }); - }); + const ionSegmentViewScroll = await page.spyOnEvent('ionSegmentViewScroll'); // Programmatically dispatch a scroll event on the segment-view host element // to simulate what the browser fires when scrollLeft changes. @@ -163,13 +153,10 @@ configs({ modes: ['md'] }).forEach(({ title, config }) => { el.dispatchEvent(new Event('scroll', { bubbles: true })); }); - await page.waitForTimeout(50); + await page.waitForChanges(); - // ionSegmentViewScroll should not have fired at all (max === 0 guard), - // but if it did fire for any reason the scrollRatio must be finite. - for (const ratio of scrollRatios) { - expect(isFinite(ratio)).toBe(true); - } + // The max === 0 guard should prevent ionSegmentViewScroll from firing entirely. + expect(ionSegmentViewScroll).not.toHaveReceivedEvent(); }); test('should set correct segment button as checked and show correct content when programmatically setting the segment value', async ({ diff --git a/core/src/components/segment/segment.tsx b/core/src/components/segment/segment.tsx index 256ec21edae..1d2674ddb69 100644 --- a/core/src/components/segment/segment.tsx +++ b/core/src/components/segment/segment.tsx @@ -388,6 +388,7 @@ export class Segment implements ComponentInterface { const dispatchedFrom = ev.target as HTMLElement; const segmentViewEl = this.segmentViewEl as EventTarget; + const segmentEl = this.el; // Only update the indicator if the event was dispatched from the correct segment view From 321e918040e7062a951d031a8526507c49fdf3ef Mon Sep 17 00:00:00 2001 From: Susmita Bhowmik Date: Wed, 6 May 2026 13:19:30 -0600 Subject: [PATCH 5/5] lint(segment): fix linter error --- core/src/components/segment/segment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/segment/segment.tsx b/core/src/components/segment/segment.tsx index 1d2674ddb69..5738be2a07f 100644 --- a/core/src/components/segment/segment.tsx +++ b/core/src/components/segment/segment.tsx @@ -388,7 +388,7 @@ export class Segment implements ComponentInterface { const dispatchedFrom = ev.target as HTMLElement; const segmentViewEl = this.segmentViewEl as EventTarget; - + const segmentEl = this.el; // Only update the indicator if the event was dispatched from the correct segment view