@@ -184,11 +184,19 @@ export class Datetime implements ComponentInterface {
184184
185185 /**
186186 * Set to `true` by the scroll-mode scroll listener when the render window
187- * re-centres (i.e. `scrollWindowCenter` changes). `componentDidRender` reads
188- * this flag and restores `scrollTop` so the visible month stays in place.
187+ * re-centres. `componentDidRender` reads this flag and restores `scrollTop`
188+ * synchronously (before the browser paints) so the visible month stays in place.
189189 */
190190 private scrollModeNeedsPositionRestore = false ;
191191
192+ /**
193+ * How far (in px) the user was scrolled past the top of the target month
194+ * when the window re-centre was triggered. Captured before the re-render so
195+ * it can be re-applied afterward, preserving the exact visual position even
196+ * when months have different heights (4, 5, or 6 week rows).
197+ */
198+ private scrollModeRestoreOffset = 0 ;
199+
192200 @State ( ) showMonthAndYear = false ;
193201
194202 /** Starting year of the currently displayed year page in the grid picker. */
@@ -1195,10 +1203,9 @@ export class Datetime implements ComponentInterface {
11951203 writeTask ( ( ) => this . setWorkingParts ( { ...this . workingParts , month, year } ) ) ;
11961204
11971205 /**
1198- * Only re-centre the scroll window (and trigger a DOM re-render)
1199- * when the user is within 1 month of either edge. This prevents
1200- * the window from rebuilding on every month boundary, which would
1201- * cause a jump even after the scrollTop restore.
1206+ * Only re-centre the scroll window when the user is within 2
1207+ * months of either edge. This avoids a DOM rebuild (and the
1208+ * associated scrollTop restore) on every single month boundary.
12021209 */
12031210 const months = this . generateScrollModeMonths ( ) ;
12041211 const first = months [ 0 ] ;
@@ -1213,6 +1220,18 @@ export class Datetime implements ComponentInterface {
12131220 ( year === last . year && month === last . month ) ||
12141221 ( year === secondLastMonth . year && month === secondLastMonth . month ) ;
12151222 if ( nearStart || nearEnd ) {
1223+ /**
1224+ * Capture how far past the top of the target month the user is
1225+ * scrolled RIGHT NOW (before the DOM re-render shifts things).
1226+ * Re-applying this offset after the re-render keeps the exact
1227+ * visual position stable even when months have different row counts.
1228+ */
1229+ const targetEl = calendarBodyRef . querySelector < HTMLElement > (
1230+ `.calendar-month[data-month="${ month } "][data-year="${ year } "]`
1231+ ) ;
1232+ this . scrollModeRestoreOffset = targetEl
1233+ ? calendarBodyRef . scrollTop - targetEl . offsetTop
1234+ : 0 ;
12161235 this . scrollModeNeedsPositionRestore = true ;
12171236 this . scrollWindowCenter = { ...this . workingParts , month, year } ;
12181237 }
@@ -1647,26 +1666,24 @@ export class Datetime implements ComponentInterface {
16471666 }
16481667
16491668 /**
1650- * Scroll mode: after the ±6 window re-centres (workingParts changed via
1651- * the scroll listener), the DOM has a new set of months. Restore scrollTop
1652- * so the working month stays at the same visual position instead of jumping.
1669+ * Scroll mode: after the ±6 window re-centres, the DOM months have shifted.
1670+ * Restore scrollTop synchronously (before the browser paints) so the currently
1671+ * visible month stays in place.
1672+ *
1673+ * We use `scrollWindowCenter` (not `workingParts`) because `setWorkingParts`
1674+ * is deferred in a writeTask and is therefore stale at this point.
1675+ * `scrollWindowCenter` was set synchronously in the scroll listener and always
1676+ * holds the month/year that triggered the re-centre.
16531677 */
16541678 if ( this . scrollModeNeedsPositionRestore && this . monthNavigation === 'scroll' && calendarBodyRef ) {
16551679 this . scrollModeNeedsPositionRestore = false ;
1656- /**
1657- * The window re-centred around `scrollWindowCenter`. Restore scrollTop
1658- * so `workingParts` (the month the user was viewing) stays at the same
1659- * visual position instead of jumping to the top of the new render window.
1660- */
1661- const { workingParts } = this ;
1662- writeTask ( ( ) => {
1663- const workingMonthEl = calendarBodyRef . querySelector < HTMLElement > (
1664- `.calendar-month[data-month="${ workingParts . month } "][data-year="${ workingParts . year } "]`
1665- ) ;
1666- if ( workingMonthEl ) {
1667- calendarBodyRef . scrollTop = workingMonthEl . offsetTop ;
1668- }
1669- } ) ;
1680+ const { scrollWindowCenter, scrollModeRestoreOffset } = this ;
1681+ const targetEl = calendarBodyRef . querySelector < HTMLElement > (
1682+ `.calendar-month[data-month="${ scrollWindowCenter . month } "][data-year="${ scrollWindowCenter . year } "]`
1683+ ) ;
1684+ if ( targetEl ) {
1685+ calendarBodyRef . scrollTop = targetEl . offsetTop + scrollModeRestoreOffset ;
1686+ }
16701687 }
16711688
16721689 if ( prevPresentation === null ) {
@@ -1776,13 +1793,15 @@ export class Datetime implements ComponentInterface {
17761793 const bodyIsVisible = el . classList . contains ( 'datetime-ready' ) ;
17771794 const { isGridStyle, showMonthAndYear } = this ;
17781795
1779- if ( isGridStyle && didChangeMonth && bodyIsVisible && ! showMonthAndYear ) {
1796+ if ( isGridStyle && didChangeMonth && bodyIsVisible && ! showMonthAndYear && this . monthNavigation !== 'scroll' ) {
17801797 /**
17811798 * Only animate if:
17821799 * 1. We're using grid style (wheel style pickers should just jump to new value)
17831800 * 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to)
17841801 * 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example)
17851802 * 4. The month/year picker is not open (since you wouldn't see the animation anyway)
1803+ * 5. Not in scroll mode — scroll mode does not use the snap-based animation; the
1804+ * month is already visible in the continuous list so no programmatic scroll is needed.
17861805 */
17871806 this . animateToDate ( targetValue ) ;
17881807 } else {
@@ -1796,11 +1815,14 @@ export class Datetime implements ComponentInterface {
17961815 } ;
17971816 this . setWorkingParts ( newParts ) ;
17981817 /**
1799- * Also re-centre the scroll window so that programmatic value changes
1800- * (and the initial load) put the correct month at the centre of the
1801- * ±6 render window in scroll mode.
1818+ * Re-centre the scroll window when the month/year actually changed
1819+ * (e.g. programmatic value changes, initial load, reset). Skip when
1820+ * only the day/time changed so that clicking or deselecting a date
1821+ * within the already-visible month does not trigger a scroll jump.
18021822 */
1803- this . scrollWindowCenter = newParts ;
1823+ if ( didChangeMonth ) {
1824+ this . scrollWindowCenter = newParts ;
1825+ }
18041826 }
18051827 } ;
18061828
@@ -3139,7 +3161,11 @@ export class Datetime implements ComponentInterface {
31393161 this . setActiveParts ( referenceParts ) ;
31403162 } else {
31413163 this . activeParts = { ...activePart , ...referenceParts } ;
3142- this . animateToDate ( referenceParts ) ;
3164+ // In scroll mode the neighboring month is already visible in the
3165+ // list, so animating to it would cause an unwanted jump.
3166+ if ( this . monthNavigation !== 'scroll' ) {
3167+ this . animateToDate ( referenceParts ) ;
3168+ }
31433169 this . confirm ( ) ;
31443170 }
31453171 } else {
0 commit comments