Skip to content

Commit daafdd5

Browse files
fix: skip native Date fast path when local timezone is overridden via setLocalTimeZone (#9678)
* fix: skip native Date fast path when local timezone is overridden via setLocalTimeZone When setLocalTimeZone is called with a timezone different from the browser's, the fast paths in getTimeZoneOffset and toAbsolute incorrectly use native Date (which always reflects the browser timezone) instead of the Intl-based path. This causes incorrect offset calculations for the overridden timezone. Skip the native Date fast paths when setLocalTimeZone has been called, falling back to the correct Intl.DateTimeFormat-based computation. Fixes #9669 * fix: use before/after comparison in resetLocalTimeZone test to avoid -0 vs 0 issue in UTC CI * fix: fix lint import ordering and add isLocalTimeZoneOverridden queries tests - Fix alphabetical import sorting in conversion.ts - Export isLocalTimeZoneOverridden from public index - Add isLocalTimeZoneOverridden unit tests in queries.test.js as requested
1 parent 3b5fa6e commit daafdd5

5 files changed

Lines changed: 88 additions & 5 deletions

File tree

packages/@internationalized/date/src/conversion.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {AnyCalendarDate, AnyDateTime, AnyTime, Calendar, DateFields, Disambiguat
1717
import {CalendarDate, CalendarDateTime, Time, ZonedDateTime} from './CalendarDate';
1818
import {constrain} from './manipulation';
1919
import {getExtendedYear, GregorianCalendar} from './calendars/GregorianCalendar';
20-
import {getLocalTimeZone, isEqualCalendar} from './queries';
20+
import {getLocalTimeZone, isEqualCalendar, isLocalTimeZoneOverridden} from './queries';
2121
import {Mutable} from './utils';
2222

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

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

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

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

130134
// Don't use Date constructor here because two-digit years are interpreted in the 20th century.

packages/@internationalized/date/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export {
6666
getLocalTimeZone,
6767
setLocalTimeZone,
6868
resetLocalTimeZone,
69+
isLocalTimeZoneOverridden,
6970
startOfMonth,
7071
startOfWeek,
7172
startOfYear,

packages/@internationalized/date/src/queries.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export function getHoursInDay(a: CalendarDate, timeZone: string): number {
130130
}
131131

132132
let localTimeZone: string | null = null;
133+
let localTimeZoneOverride = false;
133134

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

143144
/** Sets the time zone identifier for the current user. */
144145
export function setLocalTimeZone(timeZone: string): void {
146+
localTimeZoneOverride = true;
145147
localTimeZone = timeZone;
146148
}
147149

148150
/** Resets the time zone identifier for the current user. */
149151
export function resetLocalTimeZone(): void {
152+
localTimeZoneOverride = false;
150153
localTimeZone = null;
151154
}
152155

156+
/** Returns whether the local time zone has been explicitly overridden via `setLocalTimeZone`. */
157+
export function isLocalTimeZoneOverridden(): boolean {
158+
return localTimeZoneOverride;
159+
}
160+
153161
/** Returns the first date of the month for the given date. */
154162
export function startOfMonth(date: ZonedDateTime): ZonedDateTime;
155163
export function startOfMonth(date: CalendarDateTime): CalendarDateTime;

packages/@internationalized/date/tests/conversion.test.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

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

1717
describe('CalendarDate conversion', function () {
1818
describe('toAbsolute', function () {
@@ -522,4 +522,51 @@ describe('CalendarDate conversion', function () {
522522
expect(toTime(dateTime)).toEqual(new Time(8, 23, 10, 80));
523523
});
524524
});
525+
526+
describe('setLocalTimeZone', function () {
527+
afterEach(() => {
528+
resetLocalTimeZone();
529+
});
530+
531+
it('should use the overridden timezone in getTimeZoneOffset instead of native Date', function () {
532+
let ms = new Date('2020-06-15T12:00:00Z').getTime();
533+
534+
// Get the offset using the Intl-based slow path for a non-local timezone
535+
let expectedOffset = getTimeZoneOffset(ms, 'Etc/GMT-10');
536+
537+
// Now override the local timezone to 'Etc/GMT-10' and verify it still computes correctly
538+
setLocalTimeZone('Etc/GMT-10');
539+
let actualOffset = getTimeZoneOffset(ms, 'Etc/GMT-10');
540+
541+
expect(actualOffset).toBe(expectedOffset);
542+
});
543+
544+
it('should use the overridden timezone in toAbsolute instead of native Date', function () {
545+
let date = new CalendarDateTime(2020, 6, 15, 12, 0, 0);
546+
547+
// Get the expected result using the Intl-based slow path for a non-local timezone
548+
let expected = toAbsolute(date, 'Etc/GMT-10');
549+
550+
// Now override the local timezone and verify it still computes correctly
551+
setLocalTimeZone('Etc/GMT-10');
552+
let actual = toAbsolute(date, 'Etc/GMT-10');
553+
554+
expect(actual).toBe(expected);
555+
});
556+
557+
it('should produce correct results after resetLocalTimeZone', function () {
558+
let ms = new Date('2020-06-15T12:00:00Z').getTime();
559+
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
560+
561+
// Get the offset before any override
562+
let offsetBefore = getTimeZoneOffset(ms, tz);
563+
564+
setLocalTimeZone('Etc/GMT-10');
565+
resetLocalTimeZone();
566+
567+
// After reset, the fast path should be restored and produce the same result
568+
let offsetAfter = getTimeZoneOffset(ms, tz);
569+
expect(offsetAfter).toBe(offsetBefore);
570+
});
571+
});
525572
});

packages/@internationalized/date/tests/queries.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
isEqualMonth,
2626
isEqualYear,
2727
IslamicUmalquraCalendar,
28+
isLocalTimeZoneOverridden,
2829
isSameDay,
2930
isSameMonth,
3031
isSameYear,
@@ -371,4 +372,26 @@ describe('queries', function () {
371372
expect(getLocalTimeZone()).toBe(systemTimeZone);
372373
});
373374
});
375+
376+
describe('isLocalTimeZoneOverridden', function () {
377+
afterEach(() => {
378+
resetLocalTimeZone();
379+
});
380+
381+
it('returns false by default', function () {
382+
expect(isLocalTimeZoneOverridden()).toBe(false);
383+
});
384+
385+
it('returns true after setLocalTimeZone', function () {
386+
setLocalTimeZone('America/Denver');
387+
expect(isLocalTimeZoneOverridden()).toBe(true);
388+
});
389+
390+
it('returns false after resetLocalTimeZone', function () {
391+
setLocalTimeZone('America/Denver');
392+
expect(isLocalTimeZoneOverridden()).toBe(true);
393+
resetLocalTimeZone();
394+
expect(isLocalTimeZoneOverridden()).toBe(false);
395+
});
396+
});
374397
});

0 commit comments

Comments
 (0)