From aaffba2ff530d240fba89962fee17c475ab6d2d5 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 20 May 2026 21:43:55 +0200 Subject: [PATCH] soma/range-calendar: fix endpoint clear + stop auto-paging on select MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes 2 bugs from the date-range-picker incidencia 2026-05-20: #1 — `RangeCalendarProvider` no longer auto-shifts the placeholder when a selection lands in the last visible month. The visible months stay where the user put them; navigation is explicit (prev/next/month/year controls or keyboard). `reanchorInitialSelection` is removed and its `shift-navigate` trigger goes with it. #2 — clicking an endpoint of a completed range now drops only that endpoint and re-anchors on the surviving one. The previous behavior cleared both endpoints, which forced users to rebuild the entire range to amend it. The provider already implemented this; only the test codified the old behavior. Test rewritten to match the documented intent and symmetric for start/end. The two range-calendar tests that previously asserted the wrong behavior now cover the correct invariants. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../range-calendar-provider.svelte.test.ts | 28 +++++++++++++++---- .../range-calendar-provider.svelte.ts | 17 +++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.test.ts b/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.test.ts index e3c3f79ed..2f1b6914c 100644 --- a/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.test.ts +++ b/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.test.ts @@ -191,7 +191,11 @@ describe('RangeCalendarProvider', () => { dom.dispose(); }); - it('clears a completed range when an endpoint is clicked again', () => { + it('clears only the clicked endpoint of a completed range, keeping the other', () => { + // Incidencia 2026-05-20 #2 — clicking the end of a complete range must + // drop just that endpoint and re-anchor on the surviving start (the + // user is amending the range, not discarding it). The symmetric click + // on the start must drop only the start and keep the end. const { dom } = installSomaHarness(); const opts = createRangeCalendarOpts(); const start = new CalendarDate(2026, 5, 10); @@ -205,9 +209,17 @@ describe('RangeCalendarProvider', () => { provider.select(end); expect(opts.value.current).toEqual({ start, end }); + // Click end → end drops, start stays, anchor returns to start. provider.select(end); - expect(opts.value.current).toEqual({ start: undefined, end: undefined }); - expect(provider.anchor).toBeUndefined(); + expect(opts.value.current).toEqual({ start, end: undefined }); + expect(provider.anchor).toEqual(start); + + // Re-close the range, then click start → start drops, end stays. + provider.select(end); + expect(opts.value.current).toEqual({ start, end }); + provider.select(start); + expect(opts.value.current).toEqual({ start: undefined, end }); + expect(provider.anchor).toEqual(end); cleanup(); dom.dispose(); @@ -239,7 +251,11 @@ describe('RangeCalendarProvider', () => { dom.dispose(); }); - it('advances only the initial selection made in the last visible month', () => { + it('never auto-pages the placeholder on selection (incidencia 2026-05-20 #1)', () => { + // Selection must not re-anchor the visible months. The user controls + // what's on screen via prev/next/month/year — clicking a date in any + // position (including the last visible month) keeps the popup where + // they put it. const { dom } = installSomaHarness(); const opts = createRangeCalendarOpts(); opts.numberOfMonths.current = 2; @@ -256,12 +272,14 @@ describe('RangeCalendarProvider', () => { expect(opts.value.current).toEqual({ start: mayStart, end: juneEnd }); expect(opts.placeholder.current.toString()).toBe('2026-05-01'); + // A new selection that lands in the last visible month (June) used to + // shift the popup to June+July. It must stay put. provider.select(new CalendarDate(2026, 6, 20)); expect(opts.value.current).toEqual({ start: new CalendarDate(2026, 6, 20), end: undefined }); - expect(opts.placeholder.current.toString()).toBe('2026-06-01'); + expect(opts.placeholder.current.toString()).toBe('2026-05-01'); cleanup(); dom.dispose(); diff --git a/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts b/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts index dff3d269a..9a241f03b 100644 --- a/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts +++ b/src/uix/soma/components/range-calendar/range-calendar-provider.svelte.ts @@ -14,7 +14,6 @@ import { type WeekStartsOn, type DateOrder, isSameDay, - isSameMonth, startOfMonth, endOfMonth, getLocalTimeZone, @@ -378,10 +377,13 @@ export class RangeCalendarProvider { } private startSelection(date: DateValue, target?: HTMLElement): void { + // Incidencia 2026-05-20 #1 — never auto-shift the placeholder on + // selection. The visible months stay where the user put them; if they + // need to see a different range they navigate explicitly via prev/next + // or month/year selects. Click-to-select must not page the popup. this.anchor = date; this.previewDate = undefined; this.opts.value.current = { start: date, end: undefined }; - this.reanchorInitialSelection(date); void this.runtime.trigger('commit-start', target ? { fallbackTarget: target } : undefined); } @@ -419,17 +421,6 @@ export class RangeCalendarProvider { void this.runtime.trigger('commit-clear', target ? { fallbackTarget: target } : undefined); } - private reanchorInitialSelection(date: DateValue): void { - const count = Math.max(1, this.opts.numberOfMonths.current); - if (count < 2) return; - const visible = this.visibleMonths; - const last = visible[visible.length - 1]; - if (!last || !isSameMonth(date, last)) return; - this.opts.placeholder.current = startOfMonth(date); - this.announceNavigation(); - void this.runtime.trigger('shift-navigate'); - } - private commit(a: DateValue, b: DateValue, target?: HTMLElement): void { const [start, end] = orderPair(a, b);