Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/swift-ears-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@commercetools-uikit/date-time-input': patch
'@commercetools-uikit/calendar-time-utils': patch
---

Enhanced `getMonthCalendarLabel` and `getYearCalendarLabel` functions to accept optional timezone parameter. When provided, these functions now interpret dates in the specified timezone context rather than UTC, fixing display issues for timezones that are significantly ahead of or behind UTC.
98 changes: 98 additions & 0 deletions packages/calendar-time-utils/src/calendar-time.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {
getLocalizedDateTimeFormatPattern,
formatDefaultTime,
getMonthCalendarLabel,
getYearCalendarLabel,
getToday,
} from './calendar-time';
import { warning } from '@commercetools-uikit/utils';

Expand Down Expand Up @@ -101,3 +104,98 @@ describe('formatDefaultTime', () => {
);
});
});

describe('getMonthCalendarLabel', () => {
it('should return the month label without timezone', () => {
const date = '2025-01-01T10:00:00.000Z'; // January 1, 2025 in UTC
expect(getMonthCalendarLabel(date, 'en')).toBe('January');
});

it('should return the month label with timezone that shows correct month when timezone conversion would change the day', () => {
// This represents January 1st, 2025 in Pacific/Kiritimati (UTC+14)
// When converted to UTC, this becomes December 31st, 2024
const dateInKiritimati = '2024-12-31T10:00:00.000Z'; // This is January 1st in Pacific/Kiritimati

// Without timezone, it would show December (wrong)
expect(getMonthCalendarLabel(dateInKiritimati, 'en')).toBe('December');

// With timezone, it should show January (correct)
expect(
getMonthCalendarLabel(dateInKiritimati, 'en', 'Pacific/Kiritimati')
).toBe('January');
});

it('should work with different locales and timezones', () => {
const dateInUTC = '2025-06-15T12:00:00.000Z'; // June 15, 2025 in UTC

expect(getMonthCalendarLabel(dateInUTC, 'en', 'UTC')).toBe('June');
expect(getMonthCalendarLabel(dateInUTC, 'de', 'UTC')).toBe('Juni');
expect(getMonthCalendarLabel(dateInUTC, 'fr', 'UTC')).toBe('juin');
});
});

describe('getYearCalendarLabel', () => {
it('should return the year label without timezone', () => {
const date = '2025-01-01T10:00:00.000Z'; // January 1, 2025 in UTC
expect(getYearCalendarLabel(date, 'en')).toBe('2025');
});

it('should return the year label with timezone that shows correct year when timezone conversion would change the day', () => {
// This represents January 1st, 2025 in Pacific/Kiritimati (UTC+14)
// When converted to UTC, this becomes December 31st, 2024
const dateInKiritimati = '2024-12-31T10:00:00.000Z'; // This is January 1st, 2025 in Pacific/Kiritimati

// Without timezone, it would show 2024 (wrong)
expect(getYearCalendarLabel(dateInKiritimati, 'en')).toBe('2024');

// With timezone, it should show 2025 (correct)
expect(
getYearCalendarLabel(dateInKiritimati, 'en', 'Pacific/Kiritimati')
).toBe('2025');
});
});

describe('timezone edge cases', () => {
beforeEach(() => {
// Mock the current date to January 1st, 2025 in Pacific/Kiritimati
const mockDate = new Date('2025-01-01T02:00:00+14:00'); // 2025-01-01 02:00 in UTC+14 = 2024-12-31 12:00 in UTC
jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime());

// Mock moment to return this specific date when getting "now"
jest.doMock('moment-timezone', () => {
const actualMoment = jest.requireActual('moment-timezone');
return {
...actualMoment,
tz: jest.fn().mockImplementation((...args) => {
if (args[0] === 'Pacific/Kiritimati' && args.length === 1) {
// Return January 1st, 2025 in that timezone
return actualMoment.tz('2025-01-01 02:00', 'Pacific/Kiritimati');
}
return actualMoment.tz.apply(actualMoment, args);
}),
};
});
});

afterEach(() => {
jest.restoreAllMocks();
jest.dontMock('moment-timezone');
});

it('should display correct month when today in timezone converts to yesterday in UTC', () => {
// Test the scenario from the bug report:
// It's January 1st in Pacific/Kiritimati, but December 31st in UTC

const today = getToday('Pacific/Kiritimati');

// The month label should show January when using timezone
expect(getMonthCalendarLabel(today, 'en', 'Pacific/Kiritimati')).toBe(
'January'
);

// The year label should show 2025 when using timezone
expect(getYearCalendarLabel(today, 'en', 'Pacific/Kiritimati')).toBe(
'2025'
);
});
});
20 changes: 16 additions & 4 deletions packages/calendar-time-utils/src/calendar-time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,22 @@ export const getWeekdayNames = (locale: string) => {
export const getStartOf = (day: MomentInput, timeZone: string) =>
moment.tz(day, timeZone).startOf('day').toISOString();

export const getMonthCalendarLabel = (day: MomentInput, locale: string) =>
moment(day, moment.ISO_8601, locale).format('MMMM');
export const getYearCalendarLabel = (day: MomentInput, locale: string) =>
moment(day, moment.ISO_8601, locale).format('YYYY');
export const getMonthCalendarLabel = (
day: MomentInput,
locale: string,
timeZone?: string
) =>
timeZone
? moment.tz(day, timeZone).locale(locale).format('MMMM')
: moment(day, moment.ISO_8601, locale).format('MMMM');
export const getYearCalendarLabel = (
day: MomentInput,
locale: string,
timeZone?: string
) =>
timeZone
? moment.tz(day, timeZone).locale(locale).format('YYYY')
: moment(day, moment.ISO_8601, locale).format('YYYY');
export const isSameDay = (a: MomentInput, b: MomentInput) =>
moment(a).isSame(b, 'day');
export const getCalendarDayLabel = (day: MomentInput, timeZone: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,82 @@ describe('date picker defaultDaySelectionTime prop', () => {
});
});

describe('timezone edge cases', () => {
beforeEach(() => {
// Mock the current date to January 1st in a timezone ahead of UTC
// such that "today" in that timezone is "yesterday" in UTC
jest.useFakeTimers();
// Set system time to January 1st, 2025 02:00 in Pacific/Kiritimati (UTC+14)
// This represents 2024-12-31 12:00:00.000Z in UTC
jest.setSystemTime(new Date('2024-12-31T12:00:00.000Z'));
});

afterEach(() => {
jest.useRealTimers();
});

it('should display the correct month when timezone is ahead of UTC and would show wrong month', () => {
// Render DateTimeInput with Pacific/Kiritimati timezone (UTC+14)
// On January 1st in this timezone, UTC shows December 31st
const { getByLabelText } = renderDateTimeInput({
timeZone: 'Pacific/Kiritimati',
value: '', // No initial value, so calendar will show "today"
});

const dateInput = getByLabelText('Date');
fireEvent.click(dateInput);

// The calendar should show January (correct month in the timezone)
// not December (which would be wrong - the UTC month)
expect(screen.getByText('January')).toBeInTheDocument();
expect(screen.queryByText('December')).not.toBeInTheDocument();

// The year should also be correct (2025, not 2024)
expect(screen.getByText('2025')).toBeInTheDocument();
});

it('should highlight the correct day as today when timezone causes date shift', () => {
const { getByLabelText } = renderDateTimeInput({
timeZone: 'Pacific/Kiritimati',
value: '',
});

const dateInput = getByLabelText('Date');
fireEvent.click(dateInput);

// In Pacific/Kiritimati timezone, it should be January 1st
// We should find a calendar day with "1" that is marked as today
const todayElement = screen.getByText('1');
expect(todayElement).toBeInTheDocument();

// Verify the calendar is showing January 2025 (not December 2024)
expect(screen.getByText('January')).toBeInTheDocument();
expect(screen.getByText('2025')).toBeInTheDocument();
});

it('should display the correct month when value is set and timezone conversion changes the month', () => {
// This represents July 1st, 2025 in Pacific/Kiritimati (UTC+14)
// When converted to UTC, this becomes June 30th, 2025
const valueInKiritimati = '2025-06-30T10:00:00.000Z'; // This is July 1st in Pacific/Kiritimati

const { getByLabelText } = renderDateTimeInput({
timeZone: 'Pacific/Kiritimati',
value: valueInKiritimati,
});

const dateInput = getByLabelText('Date');
fireEvent.click(dateInput);

// The calendar should show July (correct month in the timezone)
// not June (which would be wrong - the UTC month)
expect(screen.getByText('July')).toBeInTheDocument();
expect(screen.queryByText('June')).not.toBeInTheDocument();

// The year should also be correct (2025)
expect(screen.getByText('2025')).toBeInTheDocument();
});
});

it('should only emit valid datetimes from manually entered datestrings', async () => {
// Render the input with an initial value
renderDateTimeInput({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ class DateTimeInput extends Component<
calendarDate:
this.props.value === ''
? getToday(this.props.timeZone)
: this.props.value,
: getStartOf(this.props.value, this.props.timeZone),
};
}

Expand Down Expand Up @@ -587,11 +587,13 @@ class DateTimeInput extends Component<
<CalendarHeader
monthLabel={getMonthCalendarLabel(
this.state.calendarDate,
this.props.intl.locale
this.props.intl.locale,
this.props.timeZone
)}
yearLabel={getYearCalendarLabel(
this.state.calendarDate,
this.props.intl.locale
this.props.intl.locale,
this.props.timeZone
)}
onPrevMonthClick={() => this.jumpMonths(-1)}
onTodayClick={this.showToday}
Expand Down
Loading