Skip to content

Commit 8bb1b77

Browse files
committed
fix(NcDateTimePicker): improve some usability issues
1 parent 9cf17af commit 8bb1b77

1 file changed

Lines changed: 89 additions & 3 deletions

File tree

src/components/NcDateTimePicker/NcDateTimePicker.vue

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ import {
288288
getFirstDay,
289289
} from '@nextcloud/l10n'
290290
import VueDatePicker from '@vuepic/vue-datepicker'
291-
import { computed, useTemplateRef } from 'vue'
291+
import { computed, ref, useTemplateRef } from 'vue'
292292
import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue'
293293
import NcTimezonePicker from '../NcTimezonePicker/NcTimezonePicker.vue'
294294
import { 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

Comments
 (0)