Describe the bug
When creating or editing an Event content type with the whole_day option enabled, the date selected in the calendar can be persisted as the previous day, depending on the user's timezone (specifically, timezones behind UTC, such as Brazil - UTC-3).
This happens because DatetimeWidget's onDateChange handler builds an internal moment object with a safe default time of noon (defaultTimeDateOnly = { hour: 12, minute: 0, second: 0 }) specifically to avoid timezone rollover issues, but then discards that time when isDateOnly is true, serializing only base.format('YYYY-MM-DD') (e.g. "2026-07-08") instead of a full ISO string.
Since this date-only string has no timezone information, the backend normalizes it assuming UTC midnight (2026-07-08T00:00:00Z). When the frontend later reads this value back and converts it to the user's local timezone (e.g. UTC-3), it rolls back to the previous day (2026-07-07T21:00:00-03:00), so the UI displays the wrong date.
To Reproduce
Steps to reproduce the behavior:
- Set your browser/OS timezone to one behind UTC (e.g. America/Sao_Paulo, UTC-3).
- Go to an Event content type add/edit form in Volto.
- Enable the "Whole day" option.
- Set the Start (or End) date to any date, e.g. July 8, 2026.
- Save the item.
- See that the date field now shows July 7, 2026 instead of July 8, 2026.
Expected behavior
The date selected by the user for a whole-day event should be preserved regardless of the browser's timezone, and should not shift by one day when reloaded.
Screenshots
N/A
Software (please complete the following information):
- OS: any
- Browser: any
- Volto Version: 19
- Plone Version: 6.2
- Plone REST API Version: 10
Additional context
The relevant code is in DatetimeWidget's onDateChange handler:
const onDateChange = (date) => {
if (date) {
const isDateOnly = getDateOnly();
const base = (getInternalValue() || moment.default()).set({
year: date.year(),
month: date.month(),
date: date.date(),
...(isDateOnly ? defaultTimeDateOnly : {}),
});
const dateValue = isDateOnly
? base.format('YYYY-MM-DD') // <- discards the safe noon time set above
: base.toISOString();
onChange(id, dateValue);
}
setIsDefault(false);
};
|
const dateValue = isDateOnly |
|
? base.format('YYYY-MM-DD') |
|
: base.toISOString(); |
|
onChange(id, dateValue); |
The defaultTimeDateOnly object (noon local time) appears to have been introduced precisely to avoid date rollover when converting to/from UTC, but the isDateOnly branch bypasses it by formatting only the date portion. A possible fix is to always serialize with base.toISOString(), keeping the whole_day flag purely as a UI concern (hiding the time picker) rather than changing what is sent to the backend:
const dateValue = base.toISOString();
onChange(id, dateValue);
This preserves the calendar day across virtually all timezones, since noon ± any realistic UTC offset (up to ±12h) does not cross a day boundary.
Describe the bug
When creating or editing an Event content type with the
whole_dayoption enabled, the date selected in the calendar can be persisted as the previous day, depending on the user's timezone (specifically, timezones behind UTC, such as Brazil - UTC-3).This happens because
DatetimeWidget'sonDateChangehandler builds an internal moment object with a safe default time of noon (defaultTimeDateOnly = { hour: 12, minute: 0, second: 0 }) specifically to avoid timezone rollover issues, but then discards that time whenisDateOnlyis true, serializing onlybase.format('YYYY-MM-DD')(e.g."2026-07-08") instead of a full ISO string.Since this date-only string has no timezone information, the backend normalizes it assuming UTC midnight (
2026-07-08T00:00:00Z). When the frontend later reads this value back and converts it to the user's local timezone (e.g. UTC-3), it rolls back to the previous day (2026-07-07T21:00:00-03:00), so the UI displays the wrong date.To Reproduce
Steps to reproduce the behavior:
Expected behavior
The date selected by the user for a whole-day event should be preserved regardless of the browser's timezone, and should not shift by one day when reloaded.
Screenshots
N/A
Software (please complete the following information):
Additional context
The relevant code is in
DatetimeWidget'sonDateChangehandler:volto/packages/volto/src/components/manage/Widgets/DatetimeWidget.jsx
Lines 137 to 140 in 8b576ce
The
defaultTimeDateOnlyobject (noon local time) appears to have been introduced precisely to avoid date rollover when converting to/from UTC, but theisDateOnlybranch bypasses it by formatting only the date portion. A possible fix is to always serialize withbase.toISOString(), keeping thewhole_dayflag purely as a UI concern (hiding the time picker) rather than changing what is sent to the backend:This preserves the calendar day across virtually all timezones, since noon ± any realistic UTC offset (up to ±12h) does not cross a day boundary.