From 62897720ad4f07fd2e56edb6745424edc2a436d4 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Wed, 2 Sep 2026 10:58:31 +0300 Subject: [PATCH 1/3] fix(ui5-dynamic-date-range): correct off-by-one-day date shift in negative UTC offsets Issue: When selecting dates using the DATE or DATERANGE options, the resolved values were shifted one day earlier than what the user selected in the calendar. This occurred in browsers running in negative UTC-offset timezones (e.g. America/Los_Angeles, America/Monterrey). The calendar fires selectedDates as Unix timestamps in seconds representing UTC midnight. Both SingleDate and DateRange converted these directly via new Date(ts * 1000) / UI5Date.getInstance(ts * 1000), producing a Date at UTC midnight. In a GMT-6 environment, that instant is the previous day at 18:00 local time. The subsequent setHours(0, 0, 0, 0) call in toDates() then locked in the wrong local day. Solution: Replace the raw timestamp-to-Date conversion in handleSelectionChange with CalendarDate.fromTimestamp(ts * 1000).toLocalJSDate(). CalendarDate is a date-only abstraction that stores the day in UTC internally; toLocalJSDate() re-applies the UTC year/month/date as local date fields, ensuring the resulting Date always represents the correct calendar day at local midnight regardless of timezone offset. This mirrors the same pattern already used in CalendarDate internally. Fixes: https://github.com/UI5/webcomponents/issues/14005 --- packages/main/src/dynamic-date-range-options/DateRange.ts | 5 +++-- packages/main/src/dynamic-date-range-options/SingleDate.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/main/src/dynamic-date-range-options/DateRange.ts b/packages/main/src/dynamic-date-range-options/DateRange.ts index 8f9e08a0821cc..16187c596b4a7 100644 --- a/packages/main/src/dynamic-date-range-options/DateRange.ts +++ b/packages/main/src/dynamic-date-range-options/DateRange.ts @@ -2,6 +2,7 @@ import DateRangeTemplate from "./DateRangeTemplate.js"; import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js"; import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; import UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js"; +import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js"; import { DYNAMIC_DATE_RANGE_DATERANGE_TEXT, @@ -85,11 +86,11 @@ class DateRange implements IDynamicDateRangeOption { currentValue.operator = this.operator; if (e.detail.selectedDates[0]) { - currentValue.values[0] = UI5Date.getInstance(e.detail.selectedDates[0] * 1000); + currentValue.values[0] = CalendarDate.fromTimestamp(e.detail.selectedDates[0] * 1000).toLocalJSDate(); } if (e.detail.selectedDates[1]) { - currentValue.values[1] = UI5Date.getInstance(e.detail.selectedDates[1] * 1000); + currentValue.values[1] = CalendarDate.fromTimestamp(e.detail.selectedDates[1] * 1000).toLocalJSDate(); } // Handle backwards date ranges by automatically flipping them diff --git a/packages/main/src/dynamic-date-range-options/SingleDate.ts b/packages/main/src/dynamic-date-range-options/SingleDate.ts index 5e9b634637990..865a6e7a358d1 100644 --- a/packages/main/src/dynamic-date-range-options/SingleDate.ts +++ b/packages/main/src/dynamic-date-range-options/SingleDate.ts @@ -1,5 +1,6 @@ import SingleDateTemplate from "./SingleDateTemplate.js"; import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; +import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js"; import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js"; import { @@ -76,7 +77,7 @@ class SingleDate implements IDynamicDateRangeOption { currentValue.operator = this.operator; if (e.detail.selectedDates[0]) { - currentValue.values[0] = new Date(e.detail.selectedDates[0] * 1000); + currentValue.values[0] = CalendarDate.fromTimestamp(e.detail.selectedDates[0] * 1000).toLocalJSDate(); } return currentValue; From a42a1481fddc2ac43d4d895522d6b60d7b8ea4bc Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Wed, 2 Sep 2026 11:32:22 +0300 Subject: [PATCH 2/3] fix(ui5-dynamic-date-range)fix(ui5-dynamic-date-range): fix lint errors --- packages/main/src/dynamic-date-range-options/DateRange.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/dynamic-date-range-options/DateRange.ts b/packages/main/src/dynamic-date-range-options/DateRange.ts index 16187c596b4a7..dc8d6c5096082 100644 --- a/packages/main/src/dynamic-date-range-options/DateRange.ts +++ b/packages/main/src/dynamic-date-range-options/DateRange.ts @@ -1,7 +1,7 @@ import DateRangeTemplate from "./DateRangeTemplate.js"; import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js"; import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; -import UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js"; +import type UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js"; import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js"; import { From 029e977c4ca0938c4293e22c42967894f75bb7f5 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Fri, 4 Sep 2026 11:25:46 +0300 Subject: [PATCH 3/3] fix(ui5-dynamic-date-range): correct off-by-one-day date shift in negative UTC offsets Issue: When selecting dates using the DATE, DATERANGE, FROMDATETIME or TODATETIME options, the returned dates were one day earlier than what the user picked in the calendar. This only affected users in timezones behind UTC (e.g. Americas), where the internal calendar timestamp was being interpreted as the previous local day. Solution: The timestamp coming from the calendar represents a specific calendar day in UTC. Instead of converting it directly to a JavaScript Date (which shifts it into local time), we now use the existing CalendarDate abstraction to extract the correct day and produce a local Date that always matches what the user selected, regardless of timezone. The conversion is extracted into a shared calendarTimestampToLocalDate helper in toDates.ts so all affected options use a single consistent implementation. A unit test for calendarTimestampToLocalDate is included, validating that the local date components of the result always match the UTC date of the calendar timestamp. Note: the test is most meaningful in negative UTC-offset environments; to verify locally run the calendar selection tests with TZ=America/Los_Angeles. Fixes: #14005 --- .../main/cypress/specs/DynamicDateRange.cy.tsx | 14 ++++++++++++++ .../src/dynamic-date-range-options/DateRange.ts | 7 +++---- .../src/dynamic-date-range-options/FromDateTime.ts | 9 ++++----- .../src/dynamic-date-range-options/SingleDate.ts | 5 ++--- .../src/dynamic-date-range-options/ToDateTime.ts | 9 ++++----- .../main/src/dynamic-date-range-options/toDates.ts | 6 ++++++ 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/packages/main/cypress/specs/DynamicDateRange.cy.tsx b/packages/main/cypress/specs/DynamicDateRange.cy.tsx index 792fe4c5fbf4f..0b97a954dfed6 100644 --- a/packages/main/cypress/specs/DynamicDateRange.cy.tsx +++ b/packages/main/cypress/specs/DynamicDateRange.cy.tsx @@ -3,6 +3,7 @@ import Label from "../../src/Label.js"; import SingleDate from '../../src/dynamic-date-range-options/SingleDate.js'; import DateRange from '../../src/dynamic-date-range-options/DateRange.js'; import Today from '../../src/dynamic-date-range-options/Today.js'; +import { calendarTimestampToLocalDate } from '../../src/dynamic-date-range-options/toDates.js'; import LastOptions from '../../src/dynamic-date-range-options/LastOptions.js'; import NextOptions from '../../src/dynamic-date-range-options/NextOptions.js'; import DateTimeRange from '../../src/dynamic-date-range-options/DateTimeRange.js'; @@ -888,4 +889,17 @@ describe("DynamicDateRange DateTimeRange Option", () => { .should("contain.value", "Mar 5, 2025") .and("contain.value", "Mar 6, 2025"); }); +}); + +describe("calendarTimestampToLocalDate", () => { + it("local date components match the UTC date of the calendar timestamp", () => { + // 1747267200 = 2025-05-15 00:00:00 UTC — same timestamp the calendar fires for May 15 + const ts = 1747267200; + const result = calendarTimestampToLocalDate(ts); + const ref = new Date(ts * 1000); + + expect(result.getFullYear()).to.equal(ref.getUTCFullYear()); + expect(result.getMonth()).to.equal(ref.getUTCMonth()); + expect(result.getDate()).to.equal(ref.getUTCDate()); + }); }); \ No newline at end of file diff --git a/packages/main/src/dynamic-date-range-options/DateRange.ts b/packages/main/src/dynamic-date-range-options/DateRange.ts index dc8d6c5096082..3fe3f4293483b 100644 --- a/packages/main/src/dynamic-date-range-options/DateRange.ts +++ b/packages/main/src/dynamic-date-range-options/DateRange.ts @@ -2,12 +2,11 @@ import DateRangeTemplate from "./DateRangeTemplate.js"; import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js"; import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; import type UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js"; -import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js"; import { DYNAMIC_DATE_RANGE_DATERANGE_TEXT, } from "../generated/i18n/i18n-defaults.js"; -import { dateRangeOptionToDates } from "./toDates.js"; +import { dateRangeOptionToDates, calendarTimestampToLocalDate } from "./toDates.js"; import DynamicDateRange from "../DynamicDateRange.js"; /** @@ -86,11 +85,11 @@ class DateRange implements IDynamicDateRangeOption { currentValue.operator = this.operator; if (e.detail.selectedDates[0]) { - currentValue.values[0] = CalendarDate.fromTimestamp(e.detail.selectedDates[0] * 1000).toLocalJSDate(); + currentValue.values[0] = calendarTimestampToLocalDate(e.detail.selectedDates[0] as number); } if (e.detail.selectedDates[1]) { - currentValue.values[1] = CalendarDate.fromTimestamp(e.detail.selectedDates[1] * 1000).toLocalJSDate(); + currentValue.values[1] = calendarTimestampToLocalDate(e.detail.selectedDates[1] as number); } // Handle backwards date ranges by automatically flipping them diff --git a/packages/main/src/dynamic-date-range-options/FromDateTime.ts b/packages/main/src/dynamic-date-range-options/FromDateTime.ts index b0427266f7b34..1b7adc88fd705 100644 --- a/packages/main/src/dynamic-date-range-options/FromDateTime.ts +++ b/packages/main/src/dynamic-date-range-options/FromDateTime.ts @@ -1,7 +1,6 @@ import FromDateTimeTemplate from "./FromDateTimeTemplate.js"; import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js"; import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; -import UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js"; import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js"; import { DATETIME_PICKER_DATE_BUTTON, @@ -9,7 +8,7 @@ import { DYNAMIC_DATE_RANGE_FROM_INPUT_TEXT, DYNAMIC_DATE_RANGE_FROM_TEXT, } from "../generated/i18n/i18n-defaults.js"; -import { dateTimeOptionToDates } from "./toDates.js"; +import { dateTimeOptionToDates, calendarTimestampToLocalDate } from "./toDates.js"; import DynamicDateRange from "../DynamicDateRange.js"; /** @@ -28,7 +27,7 @@ class FromDateTime implements IDynamicDateRangeOption { constructor() { this.template = FromDateTimeTemplate; this._showTimeView = false; - this._currentDateValue = UI5Date.getInstance(); + this._currentDateValue = new Date(); } parse(value: string): DynamicDateRangeValue { @@ -155,8 +154,8 @@ class FromDateTime implements IDynamicDateRangeOption { if (target.hasAttribute("ui5-calendar")) { if (e.detail.selectedDates[0]) { - const tempDate = UI5Date.getInstance(e.detail.selectedDates[0] * 1000); - this._currentDateValue.setFullYear(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate()); + const localDate = calendarTimestampToLocalDate(e.detail.selectedDates[0] as number); + this._currentDateValue.setFullYear(localDate.getFullYear(), localDate.getMonth(), localDate.getDate()); currentValue.values = [this._currentDateValue]; } } diff --git a/packages/main/src/dynamic-date-range-options/SingleDate.ts b/packages/main/src/dynamic-date-range-options/SingleDate.ts index 865a6e7a358d1..4d71ac954da4a 100644 --- a/packages/main/src/dynamic-date-range-options/SingleDate.ts +++ b/packages/main/src/dynamic-date-range-options/SingleDate.ts @@ -1,12 +1,11 @@ import SingleDateTemplate from "./SingleDateTemplate.js"; import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; -import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js"; import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js"; import { DYNAMIC_DATE_RANGE_DATE_TEXT, } from "../generated/i18n/i18n-defaults.js"; -import { dateOptionToDates } from "./toDates.js"; +import { dateOptionToDates, calendarTimestampToLocalDate } from "./toDates.js"; import DynamicDateRange from "../DynamicDateRange.js"; /** @@ -77,7 +76,7 @@ class SingleDate implements IDynamicDateRangeOption { currentValue.operator = this.operator; if (e.detail.selectedDates[0]) { - currentValue.values[0] = CalendarDate.fromTimestamp(e.detail.selectedDates[0] * 1000).toLocalJSDate(); + currentValue.values[0] = calendarTimestampToLocalDate(e.detail.selectedDates[0] as number); } return currentValue; diff --git a/packages/main/src/dynamic-date-range-options/ToDateTime.ts b/packages/main/src/dynamic-date-range-options/ToDateTime.ts index b2c72f42b0043..c41cf9a09db53 100644 --- a/packages/main/src/dynamic-date-range-options/ToDateTime.ts +++ b/packages/main/src/dynamic-date-range-options/ToDateTime.ts @@ -1,7 +1,6 @@ import FromDateTimeTemplate from "./FromDateTimeTemplate.js"; import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js"; import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js"; -import UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js"; import type { JsxTemplate } from "@ui5/webcomponents-base/dist/index.js"; import { DATETIME_PICKER_DATE_BUTTON, @@ -9,7 +8,7 @@ import { DYNAMIC_DATE_RANGE_TO_INPUT_TEXT, DYNAMIC_DATE_RANGE_TO_TEXT, } from "../generated/i18n/i18n-defaults.js"; -import { dateTimeOptionToDates } from "./toDates.js"; +import { dateTimeOptionToDates, calendarTimestampToLocalDate } from "./toDates.js"; import DynamicDateRange from "../DynamicDateRange.js"; /** @@ -28,7 +27,7 @@ class ToDateTime implements IDynamicDateRangeOption { constructor() { this.template = FromDateTimeTemplate; this._showTimeView = false; - this._currentDateValue = UI5Date.getInstance(); + this._currentDateValue = new Date(); } parse(value: string): DynamicDateRangeValue { @@ -156,8 +155,8 @@ class ToDateTime implements IDynamicDateRangeOption { if (target.hasAttribute("ui5-calendar")) { if (e.detail.selectedDates[0]) { - const tempDate = UI5Date.getInstance(e.detail.selectedDates[0] * 1000); - this._currentDateValue.setFullYear(tempDate.getFullYear(), tempDate.getMonth(), tempDate.getDate()); + const localDate = calendarTimestampToLocalDate(e.detail.selectedDates[0] as number); + this._currentDateValue.setFullYear(localDate.getFullYear(), localDate.getMonth(), localDate.getDate()); currentValue.values = [this._currentDateValue]; } } diff --git a/packages/main/src/dynamic-date-range-options/toDates.ts b/packages/main/src/dynamic-date-range-options/toDates.ts index cfda66bf9e3c1..ea1f8bb304982 100644 --- a/packages/main/src/dynamic-date-range-options/toDates.ts +++ b/packages/main/src/dynamic-date-range-options/toDates.ts @@ -1,5 +1,10 @@ import type { DynamicDateRangeValue, IDynamicDateRangeOption } from "../DynamicDateRange.js"; import UI5Date from "@ui5/webcomponents-localization/dist/dates/UI5Date.js"; +import CalendarDate from "@ui5/webcomponents-localization/dist/dates/CalendarDate.js"; + +const calendarTimestampToLocalDate = (timestampSeconds: number): Date => { + return CalendarDate.fromTimestamp(timestampSeconds * 1000).toLocalJSDate(); +}; const dateOptionToDates = (value: DynamicDateRangeValue): Array => { if (!value || !value.values || value.values.length !== 1) { @@ -211,6 +216,7 @@ const dateTimeOptionToDates = (value: DynamicDateRangeValue): Array => { }; export { + calendarTimestampToLocalDate, dateOptionToDates, dateRangeOptionToDates, dateTimeRangeOptionToDates,