From 58cbf03b5688f841aa1dd1a71ae58dbf19c1c8b5 Mon Sep 17 00:00:00 2001 From: Grigory V Date: Tue, 28 Apr 2026 20:01:02 +0200 Subject: [PATCH] fix(NcDateTimePicker): improve some usability issues Signed-off-by: Grigory V --- .../NcDateTimePicker/NcDateTimePicker.vue | 94 ++++++++++++++++++- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/src/components/NcDateTimePicker/NcDateTimePicker.vue b/src/components/NcDateTimePicker/NcDateTimePicker.vue index a3745cf8ec..64026909f0 100644 --- a/src/components/NcDateTimePicker/NcDateTimePicker.vue +++ b/src/components/NcDateTimePicker/NcDateTimePicker.vue @@ -288,7 +288,7 @@ import { getFirstDay, } from '@nextcloud/l10n' import VueDatePicker from '@vuepic/vue-datepicker' -import { computed, useTemplateRef } from 'vue' +import { computed, ref, useTemplateRef } from 'vue' import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue' import NcTimezonePicker from '../NcTimezonePicker/NcTimezonePicker.vue' import { t } from '../../l10n.ts' @@ -698,6 +698,78 @@ const ariaLabels = computed(() => ({ yearPicker: (overlay: boolean) => overlay ? t('Year picker overlay') : t('Year picker'), })) +/** + * Track the currently displayed month/year so we can navigate on horizontal scroll. + * Initialise from modelValue so the first scroll continues from the displayed month, + * not from today. + */ +function getInitialMonthYear() { + const date = props.modelValue instanceof Date + ? props.modelValue + : (Array.isArray(props.modelValue) && props.modelValue[0] instanceof Date + ? props.modelValue[0] + : new Date()) + return { month: date.getMonth(), year: date.getFullYear() } +} +const currentMonthYear = ref(getInitialMonthYear()) + +/** + * Called when the displayed month/year changes in the library (navigation arrows, etc.) + * + * @param payload The emitted month/year object from the library + * @param payload.instance + * @param payload.month + * @param payload.year + */ +function onUpdateMonthYear(payload: { instance: number, month: number, year: number }) { + if (!Number.isNaN(payload.month) && !Number.isNaN(payload.year)) { + currentMonthYear.value = { month: payload.month, year: payload.year } + } +} + +// Timer handle used to throttle horizontal scroll — null means "ready to fire" +let scrollCooldownTimer: ReturnType | null = null +// Minimum pause (ms) between consecutive month steps triggered by scrolling +const SCROLL_STEP_COOLDOWN_MS = 500 + +/** + * Handle horizontal wheel scroll on the calendar to navigate months. + * Vertical scroll is intentionally ignored so the page can still scroll normally. + * A cooldown timer prevents more than one month step per gesture. + * + * @param event The wheel event + */ +function onCalendarWheel(event: WheelEvent) { + // Only act when horizontal component is dominant; ignore pure vertical scroll + if (Math.abs(event.deltaX) <= Math.abs(event.deltaY)) { + return + } + event.preventDefault() + + // Cooldown active — ignore until the timer expires + if (scrollCooldownTimer !== null) { + return + } + + // deltaX > 0 → scrolled right → next month (future); < 0 → previous month (past) + const direction = event.deltaX > 0 ? 1 : -1 + let { month, year } = currentMonthYear.value + month += direction + if (month > 11) { + month = 0 + year++ + } else if (month < 0) { + month = 11 + year-- + } + currentMonthYear.value = { month, year } + pickerInstance.value?.setMonthYear({ month, year }) + + scrollCooldownTimer = setTimeout(() => { + scrollCooldownTimer = null + }, SCROLL_STEP_COOLDOWN_MS) +} + /** * Select the current value. * This is used by the confirmation button if `confirmation` was set. @@ -765,7 +837,7 @@ function sameDay(a: Date, b: Date): boolean {