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
10 changes: 7 additions & 3 deletions packages/@internationalized/date/src/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {AnyCalendarDate, AnyDateTime, AnyTime, Calendar, DateFields, Disambiguat
import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate';
import {constrain} from './manipulation';
import {getExtendedYear, GregorianCalendar} from './calendars/GregorianCalendar';
import {getLocalTimeZone, isEqualCalendar} from './queries';
import {getLocalTimeZone, isEqualCalendar, isLocalTimeZoneOverridden} from './queries';
import {Mutable} from './utils';

export function epochFromDate(date: AnyDateTime): number {
Expand All @@ -42,7 +42,9 @@ export function getTimeZoneOffset(ms: number, timeZone: string): number {
}

// Fast path: for local timezone after 1970, use native Date.
if (ms > 0 && timeZone === getLocalTimeZone()) {
// Skip this fast path if the local timezone was explicitly overridden via setLocalTimeZone,
// since native Date always uses the browser's timezone, not the overridden one.
if (ms > 0 && timeZone === getLocalTimeZone() && !isLocalTimeZoneOverridden()) {
return new Date(ms).getTimezoneOffset() * -60 * 1000;
}

Expand Down Expand Up @@ -124,7 +126,9 @@ export function toAbsolute(date: CalendarDate | CalendarDateTime, timeZone: stri
}

// Fast path: if the time zone is the local timezone and disambiguation is compatible, use native Date.
if (timeZone === getLocalTimeZone() && disambiguation === 'compatible') {
// Skip this fast path if the local timezone was explicitly overridden via setLocalTimeZone,
// since native Date always uses the browser's timezone, not the overridden one.
if (timeZone === getLocalTimeZone() && disambiguation === 'compatible' && !isLocalTimeZoneOverridden()) {
dateTime = toCalendar(dateTime, new GregorianCalendar());

// Don't use Date constructor here because two-digit years are interpreted in the 20th century.
Expand Down
1 change: 1 addition & 0 deletions packages/@internationalized/date/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export {
getLocalTimeZone,
setLocalTimeZone,
resetLocalTimeZone,
isLocalTimeZoneOverridden,
startOfMonth,
startOfWeek,
startOfYear,
Expand Down
8 changes: 8 additions & 0 deletions packages/@internationalized/date/src/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export function getHoursInDay(a: CalendarDate, timeZone: string): number {
}

let localTimeZone: string | null = null;
let localTimeZoneOverride = false;

/** Returns the time zone identifier for the current user. */
export function getLocalTimeZone(): string {
Expand All @@ -142,14 +143,21 @@ export function getLocalTimeZone(): string {

/** Sets the time zone identifier for the current user. */
export function setLocalTimeZone(timeZone: string): void {
localTimeZoneOverride = true;
localTimeZone = timeZone;
}

/** Resets the time zone identifier for the current user. */
export function resetLocalTimeZone(): void {
localTimeZoneOverride = false;
localTimeZone = null;
}

/** Returns whether the local time zone has been explicitly overridden via `setLocalTimeZone`. */
export function isLocalTimeZoneOverridden(): boolean {
return localTimeZoneOverride;
}

/** Returns the first date of the month for the given date. */
export function startOfMonth(date: ZonedDateTime): ZonedDateTime;
export function startOfMonth(date: CalendarDateTime): CalendarDateTime;
Expand Down
51 changes: 49 additions & 2 deletions packages/@internationalized/date/tests/conversion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
* governing permissions and limitations under the License.
*/

import {BuddhistCalendar, CalendarDate, CalendarDateTime, EthiopicAmeteAlemCalendar, EthiopicCalendar, GregorianCalendar, HebrewCalendar, IndianCalendar, IslamicCivilCalendar, IslamicTabularCalendar, IslamicUmalquraCalendar, JapaneseCalendar, PersianCalendar, TaiwanCalendar, Time, toCalendar, toCalendarDate, toCalendarDateTime, toTime, ZonedDateTime} from '..';
import {BuddhistCalendar, CalendarDate, CalendarDateTime, EthiopicAmeteAlemCalendar, EthiopicCalendar, GregorianCalendar, HebrewCalendar, IndianCalendar, IslamicCivilCalendar, IslamicTabularCalendar, IslamicUmalquraCalendar, JapaneseCalendar, PersianCalendar, resetLocalTimeZone, setLocalTimeZone, TaiwanCalendar, Time, toCalendar, toCalendarDate, toCalendarDateTime, toTime, ZonedDateTime} from '..';
import {Custom454Calendar} from './customCalendarImpl';
import {fromAbsolute, possibleAbsolutes, toAbsolute, toDate} from '../src/conversion';
import {fromAbsolute, getTimeZoneOffset, possibleAbsolutes, toAbsolute, toDate} from '../src/conversion';

describe('CalendarDate conversion', function () {
describe('toAbsolute', function () {
Expand Down Expand Up @@ -522,4 +522,51 @@ describe('CalendarDate conversion', function () {
expect(toTime(dateTime)).toEqual(new Time(8, 23, 10, 80));
});
});

describe('setLocalTimeZone', function () {
afterEach(() => {
resetLocalTimeZone();
});

it('should use the overridden timezone in getTimeZoneOffset instead of native Date', function () {
let ms = new Date('2020-06-15T12:00:00Z').getTime();

// Get the offset using the Intl-based slow path for a non-local timezone
let expectedOffset = getTimeZoneOffset(ms, 'Etc/GMT-10');

// Now override the local timezone to 'Etc/GMT-10' and verify it still computes correctly
setLocalTimeZone('Etc/GMT-10');
let actualOffset = getTimeZoneOffset(ms, 'Etc/GMT-10');

expect(actualOffset).toBe(expectedOffset);
});

it('should use the overridden timezone in toAbsolute instead of native Date', function () {
let date = new CalendarDateTime(2020, 6, 15, 12, 0, 0);

// Get the expected result using the Intl-based slow path for a non-local timezone
let expected = toAbsolute(date, 'Etc/GMT-10');

// Now override the local timezone and verify it still computes correctly
setLocalTimeZone('Etc/GMT-10');
let actual = toAbsolute(date, 'Etc/GMT-10');

expect(actual).toBe(expected);
});

it('should produce correct results after resetLocalTimeZone', function () {
let ms = new Date('2020-06-15T12:00:00Z').getTime();
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;

// Get the offset before any override
let offsetBefore = getTimeZoneOffset(ms, tz);

setLocalTimeZone('Etc/GMT-10');
resetLocalTimeZone();

// After reset, the fast path should be restored and produce the same result
let offsetAfter = getTimeZoneOffset(ms, tz);
expect(offsetAfter).toBe(offsetBefore);
});
});
});
23 changes: 23 additions & 0 deletions packages/@internationalized/date/tests/queries.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isEqualMonth,
isEqualYear,
IslamicUmalquraCalendar,
isLocalTimeZoneOverridden,
isSameDay,
isSameMonth,
isSameYear,
Expand Down Expand Up @@ -371,4 +372,26 @@ describe('queries', function () {
expect(getLocalTimeZone()).toBe(systemTimeZone);
});
});

describe('isLocalTimeZoneOverridden', function () {
afterEach(() => {
resetLocalTimeZone();
});

it('returns false by default', function () {
expect(isLocalTimeZoneOverridden()).toBe(false);
});

it('returns true after setLocalTimeZone', function () {
setLocalTimeZone('America/Denver');
expect(isLocalTimeZoneOverridden()).toBe(true);
});

it('returns false after resetLocalTimeZone', function () {
setLocalTimeZone('America/Denver');
expect(isLocalTimeZoneOverridden()).toBe(true);
resetLocalTimeZone();
expect(isLocalTimeZoneOverridden()).toBe(false);
});
});
});