@@ -288,7 +288,7 @@ import {
288288 getFirstDay ,
289289} from ' @nextcloud/l10n'
290290import VueDatePicker from ' @vuepic/vue-datepicker'
291- import { computed , useTemplateRef } from ' vue'
291+ import { computed , ref , useTemplateRef } from ' vue'
292292import NcIconSvgWrapper from ' ../NcIconSvgWrapper/NcIconSvgWrapper.vue'
293293import NcTimezonePicker from ' ../NcTimezonePicker/NcTimezonePicker.vue'
294294import { t } from ' ../../l10n.ts'
@@ -698,6 +698,76 @@ const ariaLabels = computed(() => ({
698698 yearPicker : (overlay : boolean ) => overlay ? t (' Year picker overlay' ) : t (' Year picker' ),
699699}))
700700
701+ /**
702+ * Track the currently displayed month/year so we can navigate on horizontal scroll.
703+ * Initialise from modelValue so the first scroll continues from the displayed month,
704+ * not from today.
705+ */
706+ function getInitialMonthYear() {
707+ const date = props .modelValue instanceof Date
708+ ? props .modelValue
709+ : (Array .isArray (props .modelValue ) && props .modelValue [0 ] instanceof Date
710+ ? props .modelValue [0 ]
711+ : new Date ())
712+ return { month: date .getMonth (), year: date .getFullYear () }
713+ }
714+ const currentMonthYear = ref (getInitialMonthYear ())
715+
716+ /**
717+ * Called when the displayed month/year changes in the library (navigation arrows, etc.)
718+ *
719+ * @param payload The emitted month/year object from the library
720+ * @param payload.instance
721+ * @param payload.month
722+ * @param payload.year
723+ */
724+ function onUpdateMonthYear(payload : { instance: number , month: number , year: number }) {
725+ if (! Number .isNaN (payload .month ) && ! Number .isNaN (payload .year )) {
726+ currentMonthYear .value = { month: payload .month , year: payload .year }
727+ }
728+ }
729+
730+ // Timer handle used to throttle horizontal scroll — null means "ready to fire"
731+ let scrollCooldownTimer: ReturnType <typeof setTimeout > | null = null
732+ // Minimum pause (ms) between consecutive month steps triggered by scrolling
733+ const SCROLL_STEP_COOLDOWN_MS = 500
734+
735+ /**
736+ * Handle horizontal wheel scroll on the calendar to navigate months.
737+ * Vertical scroll is intentionally ignored so the page can still scroll normally.
738+ * A cooldown timer prevents more than one month step per gesture.
739+ *
740+ * @param event The wheel event
741+ */
742+ function onCalendarWheel(event : WheelEvent ) {
743+ // Only act when horizontal component is dominant; ignore pure vertical scroll
744+ if (Math .abs (event .deltaX ) <= Math .abs (event .deltaY )) {
745+ return
746+ }
747+ event .preventDefault ()
748+
749+ // Cooldown active — ignore until the timer expires
750+ if (scrollCooldownTimer !== null ) {
751+ return
752+ }
753+
754+ // deltaX > 0 → scrolled right → next month (future); < 0 → previous month (past)
755+ const direction = event .deltaX > 0 ? 1 : - 1
756+ let { month, year } = currentMonthYear .value
757+ month += direction
758+ if (month > 11 ) {
759+ month = 0
760+ year ++
761+ } else if (month < 0 ) {
762+ month = 11
763+ year --
764+ }
765+ currentMonthYear .value = { month , year }
766+ pickerInstance .value ?.setMonthYear ({ month , year })
767+
768+ scrollCooldownTimer = setTimeout (() => { scrollCooldownTimer = null }, SCROLL_STEP_COOLDOWN_MS )
769+ }
770+
701771/**
702772 * Select the current value.
703773 * This is used by the confirmation button if `confirmation` was set.
@@ -765,7 +835,7 @@ function sameDay(a: Date, b: Date): boolean {
765835 </script >
766836
767837<template >
768- <div class =" vue-date-time-picker__wrapper" >
838+ <div class =" vue-date-time-picker__wrapper" @wheel = " onCalendarWheel " >
769839 <VueDatePicker
770840 ref =" picker"
771841 :aria-labels
@@ -784,6 +854,7 @@ function sameDay(a: Date, b: Date): boolean {
784854 :maxTime =" calcMinMaxTime.maxTime"
785855 :minutesIncrement =" minuteStep"
786856 :modelValue =" value"
857+ :monthChangeOnScroll =" false"
787858 :nowButtonLabel =" t('Now')"
788859 :selectText =" t('Pick')"
789860 sixWeeks =" fair"
@@ -795,6 +866,7 @@ function sameDay(a: Date, b: Date): boolean {
795866 :weekStart
796867 v-bind =" pickerType"
797868 @update:modelValue =" onUpdateModelValue"
869+ @updateMonthYear =" onUpdateMonthYear"
798870 @blur =" emit('blur')" >
799871 <template #action-buttons >
800872 <NcButton size =" small" variant =" tertiary" @click =" cancelSelection" >
@@ -949,10 +1021,24 @@ function sameDay(a: Date, b: Date): boolean {
9491021
9501022 // make the bottom page toggle stand out better
9511023 :deep(.dp__btn.dp__button.dp__button_bottom ) {
952- color : var (--color-primary-element-light );
1024+ color : var (--color-primary-element-light-text );
9531025 background-color : var (--color-primary-element-light );
9541026 }
9551027
1028+ // Disabled days that are also selected/active should use primary text color to stay legible
1029+ // on the blue primary background, instead of the grey secondary color used for disabled days.
1030+ // Same applies to offset days (days from prev/next month shown at calendar edges).
1031+ :deep(.dp__cell_disabled.dp__active_date ),
1032+ :deep(.dp__cell_disabled.dp__range_start ),
1033+ :deep(.dp__cell_disabled.dp__range_end ),
1034+ :deep(.dp__cell_disabled.dp__range_between ),
1035+ :deep(.dp__cell_offset.dp__active_date ),
1036+ :deep(.dp__cell_offset.dp__range_start ),
1037+ :deep(.dp__cell_offset.dp__range_end ),
1038+ :deep(.dp__cell_offset.dp__range_between ) {
1039+ color : var (--dp-primary-text-color );
1040+ }
1041+
9561042 // Fix server styles causing buttons to be primary colored
9571043 :deep(.dp--header-wrap .dp__btn :not (.dp__button_bottom )),
9581044 :deep(.dp__time_col .dp__btn ) {
0 commit comments