diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateOnlyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateOnlyValueConverter.cs
index cdb2339478cc..a01e39e6fe66 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateOnlyValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateOnlyValueConverter.cs
@@ -30,5 +30,5 @@ public override bool IsConverter(IPublishedPropertyType propertyType)
///
protected override object ConvertToObject(DateTimeDto dateTimeDto)
- => DateOnly.FromDateTime(dateTimeDto.Date.UtcDateTime);
+ => DateOnly.FromDateTime(dateTimeDto.Date.DateTime);
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverter.cs
index da793aeb2f8b..9bd138e591a9 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverter.cs
@@ -30,6 +30,5 @@ public override bool IsConverter(IPublishedPropertyType propertyType)
///
protected override object ConvertToObject(DateTimeDto dateTimeDto)
- => DateTime.SpecifyKind(dateTimeDto.Date.UtcDateTime, DateTimeKind.Unspecified);
-
+ => DateTime.SpecifyKind(dateTimeDto.Date.DateTime, DateTimeKind.Unspecified);
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/TimeOnlyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/TimeOnlyValueConverter.cs
index f6862e5b6f7f..777a20f0489a 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/TimeOnlyValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/TimeOnlyValueConverter.cs
@@ -30,5 +30,5 @@ public override bool IsConverter(IPublishedPropertyType propertyType)
///
protected override object ConvertToObject(DateTimeDto dateTimeDto)
- => TimeOnly.FromDateTime(dateTimeDto.Date.UtcDateTime);
+ => TimeOnly.FromDateTime(dateTimeDto.Date.DateTime);
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts
index 8c0b1ce9aa80..07e196850aba 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts
@@ -19,8 +19,8 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
interface UmbDateTime {
- date: string | undefined;
- timeZone: string | undefined;
+ date: string | null;
+ timeZone: string | null;
}
interface UmbTimeZonePickerOption extends UmbTimeZone {
@@ -34,6 +34,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
{
private _timeZoneOptions: Array = [];
private _clientTimeZone: UmbTimeZone | undefined;
+ private _timeZoneMode: UmbTimeZonePickerValue['mode'] | undefined;
@property({ type: Boolean, reflect: true })
readonly = false;
@@ -104,6 +105,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
() => {
return (
this._displayTimeZone &&
+ this._timeZoneMode !== 'local' &&
!!this.value?.timeZone &&
!this._timeZoneOptions.some((opt) => opt.value === this.value?.timeZone && !opt.invalid)
);
@@ -120,8 +122,13 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
if (this._displayTimeZone) {
timeZonePickerConfig = config.getValueByAlias('timeZones');
}
+
this.#setTimeInputStep(timeFormat);
this.#prefillValue(timeZonePickerConfig);
+
+ // To ensure the expected value matches the prefilled value, we trigger an update.
+ // If the values match, no change event will be fired.
+ this.#updateValue(this._selectedDate?.toISO({ includeOffset: false }) ?? null);
}
#prefillValue(timeZonePickerConfig: UmbTimeZonePickerValue | undefined) {
@@ -158,8 +165,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
#prefillTimeZones(config: UmbTimeZonePickerValue | undefined, selectedDate: DateTime | undefined) {
// Retrieve the time zones from the config
this._clientTimeZone = getClientTimeZone();
+ this._timeZoneMode = config?.mode;
- // Retrieve the time zones from the config
const dateToCalculateOffset = selectedDate ?? DateTime.now();
switch (config?.mode) {
case 'all':
@@ -219,7 +226,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
return;
}
} else if (this.value?.date) {
- return; // If there is a date but no time zone, we don't preselect anything
+ // If there is no time zone in the value, but there is a date, we leave the time zone unselected
+ return;
}
// Check if we can pre-select the client time zone
@@ -269,16 +277,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
return;
}
- if (!newPickerValue) {
- this._datePickerValue = '';
- this.value = undefined;
- this._selectedDate = undefined;
- this.dispatchEvent(new UmbChangeEvent());
- return;
- }
-
this._datePickerValue = newPickerValue;
- this.#updateValue(value, true);
+ this.#updateValue(value);
}
#onTimeZoneChange(event: UUIComboboxEvent) {
@@ -291,7 +291,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
if (!this._selectedTimeZone) {
if (this.value?.date) {
- this.value = { date: this.value.date, timeZone: undefined };
+ this.value = { date: this.value.date, timeZone: null };
} else {
this.value = undefined;
}
@@ -303,46 +303,84 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
return;
}
- this.#updateValue(this._selectedDate.toISO({ includeOffset: false }) || '');
+ this.#updateValue(this._selectedDate.toISO({ includeOffset: false }));
}
- #updateValue(date: string, updateOffsets = false) {
+ #updateValue(date: string | null) {
// Try to parse the date with the selected time zone
- const newDate = DateTime.fromISO(date, { zone: this._selectedTimeZone ?? 'UTC' });
+ const newDate = date ? DateTime.fromISO(date, { zone: this._selectedTimeZone || 'UTC' }) : null;
// If the date is invalid, we reset the value
- if (!newDate.isValid) {
+ if (!newDate || !newDate.isValid) {
+ if (!this.value) {
+ return; // No change
+ }
this.value = undefined;
this._selectedDate = undefined;
this.dispatchEvent(new UmbChangeEvent());
+ this.#updateOffsets(DateTime.now());
return;
}
+ const previousDate = this._selectedDate;
this._selectedDate = newDate;
- this.value = {
- date: this.#getCurrentDateValue(),
- timeZone: this._selectedTimeZone,
+
+ let timeZoneToStore = null;
+ if (!this._displayTimeZone || !this._timeZoneMode) {
+ timeZoneToStore = null;
+ } else if (this._timeZoneMode === 'local') {
+ timeZoneToStore = 'UTC';
+ } else {
+ timeZoneToStore = this._selectedTimeZone ?? null;
+ }
+
+ const dateToStore =
+ timeZoneToStore && this._selectedTimeZone !== timeZoneToStore ? newDate.setZone(timeZoneToStore) : newDate;
+
+ const newValue = {
+ date: this.#formatDateValue(dateToStore),
+ timeZone: timeZoneToStore,
};
- if (updateOffsets) {
- this._timeZoneOptions.forEach((opt) => {
- opt.offset = getTimeZoneOffset(opt.value, newDate);
- });
- // Update the time zone options (mostly for the offset)
- this._filteredTimeZoneOptions = this._timeZoneOptions;
+ // Only update the stored data if it has actually changed to avoid firing unnecessary change events
+ const previousValue = this.value;
+ if (previousValue?.date === newValue.date && previousValue?.timeZone === newValue.timeZone) {
+ return;
}
+
+ this.value = newValue;
this.dispatchEvent(new UmbChangeEvent());
+
+ // Only update offsets if the date timestamp has changed
+ if (previousDate?.toUnixInteger() !== newDate.toUnixInteger()) {
+ this.#updateOffsets(newDate);
+ }
}
- #getCurrentDateValue(): string | undefined {
+ #updateOffsets(date: DateTime) {
+ if (!this._displayTimeZone) return;
+ this._timeZoneOptions.forEach((opt) => {
+ opt.offset = getTimeZoneOffset(opt.value, date);
+ });
+ // Update the time zone options (mostly for the offset)
+ this._filteredTimeZoneOptions = this._timeZoneOptions;
+ }
+
+ #formatDateValue(date: DateTime): string | null {
+ let formattedDate: string | undefined;
switch (this._dateInputType) {
case 'date':
- return this._selectedDate?.toISODate() ?? undefined;
+ formattedDate = date.toFormat('yyyy-MM-dd');
+ break;
case 'time':
- return this._selectedDate?.toISOTime({ includeOffset: false }) ?? undefined;
+ formattedDate = date.toFormat('HH:mm:ss');
+ break;
default:
- return this._selectedDate?.toISO({ includeOffset: !!this._selectedTimeZone }) ?? undefined;
+ formattedDate = date.toFormat(`yyyy-MM-dd'T'HH:mm:ss${this._timeZoneMode ? 'ZZ' : ''}`);
+ break;
}
+
+ return formattedDate ?? null;
}
#onTimeZoneSearch(event: UUIComboboxEvent) {
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs
index 2e5135f1ab73..48f8a035be6f 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs
@@ -38,10 +38,10 @@ protected override void CustomTestSetup(IUmbracoBuilder builder)
private static readonly object[] _sourceList1 =
[
- new object[] { Constants.PropertyEditors.Aliases.DateOnly, false, new DateOnly(2025, 1, 22) },
+ new object[] { Constants.PropertyEditors.Aliases.DateOnly, false, new DateOnly(2025, 6, 22) },
new object[] { Constants.PropertyEditors.Aliases.TimeOnly, false, new TimeOnly(18, 33, 1) },
- new object[] { Constants.PropertyEditors.Aliases.DateTimeUnspecified, false, new DateTime(2025, 1, 22, 18, 33, 1) },
- new object[] { Constants.PropertyEditors.Aliases.DateTimeWithTimeZone, true, new DateTimeOffset(2025, 1, 22, 18, 33, 1, TimeSpan.Zero) },
+ new object[] { Constants.PropertyEditors.Aliases.DateTimeUnspecified, false, new DateTime(2025, 6, 22, 18, 33, 1) },
+ new object[] { Constants.PropertyEditors.Aliases.DateTimeWithTimeZone, true, new DateTimeOffset(2025, 6, 22, 18, 33, 1, TimeSpan.FromHours(2)) },
];
[TestCaseSource(nameof(_sourceList1))]
@@ -105,7 +105,7 @@ public async Task Returns_Correct_Type_Based_On_Configuration(
.WithValue(
new JsonObject
{
- ["date"] = "2025-01-22T18:33:01.0000000+00:00",
+ ["date"] = "2025-06-22T18:33:01.0000000+02:00",
["timeZone"] = "Europe/Copenhagen",
})
.Done()
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs
index dd7395f5d020..1f38c034f265 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs
@@ -86,7 +86,7 @@ public void Can_Convert_To_Intermediate_Value(string? input, object? expected)
private static object[] _dateTimeUnspecifiedConvertToObjectCases =
[
new object[] { null, null },
- new object[] { _convertToObjectInputDate, DateTime.Parse("2025-08-20T17:30:00") },
+ new object[] { _convertToObjectInputDate, DateTime.Parse("2025-08-20T16:30:00") },
];
[TestCaseSource(nameof(_dateTimeUnspecifiedConvertToObjectCases))]
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs
index a0bcd82ec9f8..ee3da5167168 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs
@@ -86,7 +86,7 @@ public void Can_Convert_To_Intermediate_Value(string? input, object? expected)
private static object[] _timeOnlyConvertToObjectCases =
[
new object[] { null, null },
- new object[] { _convertToObjectInputDate, TimeOnly.Parse("17:30") },
+ new object[] { _convertToObjectInputDate, TimeOnly.Parse("16:30") },
];
[TestCaseSource(nameof(_timeOnlyConvertToObjectCases))]