Skip to content

Commit 2838072

Browse files
feat: Use succinct UTC time zone offset: "+00:00" -> "Z" (take 2) (#3787)
1 parent d2324bb commit 2838072

File tree

10 files changed

+55
-21
lines changed

10 files changed

+55
-21
lines changed

src/date-range-picker/__integ__/date-range-picker.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('Date Range Picker', () => {
3939
await page.keys('Enter');
4040

4141
await expect(page.getTriggerText()).resolves.toBe(
42-
granularity === 'day' ? '2018-01-16T00:00:00+00:00 — 2018-01-24T23:59:59+00:00' : '2018-01 — 2018-01'
42+
granularity === 'day' ? '2018-01-16T00:00:00Z — 2018-01-24T23:59:59Z' : '2018-01 — 2018-01'
4343
);
4444
}, granularity)
4545
);
@@ -65,7 +65,7 @@ describe('Date Range Picker', () => {
6565
await page.keys('Enter');
6666

6767
await expect(page.getTriggerText()).resolves.toBe(
68-
granularity === 'day' ? '2018-01-17T00:00:00+00:00 — 2018-01-19T15:30:00+00:00' : '2018-01 — 2018-01'
68+
granularity === 'day' ? '2018-01-17T00:00:00Z — 2018-01-19T15:30:00Z' : '2018-01 — 2018-01'
6969
);
7070
}, granularity)
7171
);

src/date-range-picker/__tests__/time-offset.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,14 @@ describe('Date range picker', () => {
5959
{ type: 'absolute', startDate: '2020-10-11T23:23:45', endDate: '2020-10-12T08:23:45' },
6060
],
6161
[
62-
'UTC and +00:00 with negative offset',
63-
{ type: 'absolute', startDate: '2020-10-12T01:23:45Z', endDate: '2020-10-12T01:23:45+00:00' },
62+
'UTC and Z with negative offset',
63+
{ type: 'absolute', startDate: '2020-10-12T01:23:45Z', endDate: '2020-10-12T01:23:45Z' },
6464
{ startDate: -240, endDate: -240 },
6565
{ type: 'absolute', startDate: '2020-10-11T21:23:45', endDate: '2020-10-11T21:23:45' },
6666
],
6767
[
6868
'UTC with mixed offsets',
69-
{ type: 'absolute', startDate: '2020-10-12T01:23:45Z', endDate: '2020-10-12T01:23:45+00:00' },
69+
{ type: 'absolute', startDate: '2020-10-12T01:23:45Z', endDate: '2020-10-12T01:23:45Z' },
7070
{ startDate: 120, endDate: -240 },
7171
{ type: 'absolute', startDate: '2020-10-12T03:23:45', endDate: '2020-10-11T21:23:45' },
7272
],

src/date-range-picker/__tests__/utils.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ describe('DateRangePicker utils', () => {
215215

216216
const expected = {
217217
type: 'absolute',
218-
startDate: '2023-06-15T00:00:00Z+60000:00',
219-
endDate: '2023-07-20T23:59:59Z+120000:00',
218+
startDate: '2023-06-15T00:00:00+60000:00',
219+
endDate: '2023-07-20T23:59:59+120000:00',
220220
};
221221

222222
const result = formatValue(input as DateRangePickerProps.Value, { ...defaultOptions, timeOffset });
@@ -235,8 +235,8 @@ describe('DateRangePicker utils', () => {
235235

236236
const expected = {
237237
type: 'absolute',
238-
startDate: '2023-06-15T00:00:00Z+00:00',
239-
endDate: '2023-07-20T23:59:59Z+00:00',
238+
startDate: '2023-06-15T00:00:00+00:00',
239+
endDate: '2023-07-20T23:59:59+00:00',
240240
};
241241

242242
const result = formatValue(input as DateRangePickerProps.Value, { ...defaultOptions, timeOffset });

src/date-range-picker/time-offset.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { addMinutes } from 'date-fns';
44

55
import { warnOnce } from '@cloudscape-design/component-toolkit/internal';
66

7-
import { formatTimeOffsetISO, parseTimezoneOffset, shiftTimezoneOffset } from '../internal/utils/date-time';
7+
import { formatTimeOffsetISOInternal, parseTimezoneOffset, shiftTimezoneOffset } from '../internal/utils/date-time';
88
import { DateRangePickerProps } from './interfaces';
99

1010
/**
11-
* Appends a time zone offset to an offset-less date string.
11+
* Appends a time zone offset to a date string, replacing any existing timezone information.
1212
*/
1313
export function setTimeOffset(
1414
value: DateRangePickerProps.Value | null,
@@ -17,10 +17,16 @@ export function setTimeOffset(
1717
if (!(value?.type === 'absolute')) {
1818
return value;
1919
}
20+
21+
const stripTimezone = (dateString: string): string => {
22+
// Remove existing timezone info: Z, +HH:MM, -HH:MM, +HHMM, -HHMM
23+
return dateString.replace(/[Z]$|[+-]\d{2}:?\d{2}$/, '');
24+
};
25+
2026
return {
2127
type: 'absolute',
22-
startDate: value.startDate + formatTimeOffsetISO(value.startDate, timeOffset.startDate),
23-
endDate: value.endDate + formatTimeOffsetISO(value.endDate, timeOffset.endDate),
28+
startDate: stripTimezone(value.startDate) + formatTimeOffsetISOInternal(value.startDate, timeOffset.startDate),
29+
endDate: stripTimezone(value.endDate) + formatTimeOffsetISOInternal(value.endDate, timeOffset.endDate),
2430
};
2531
}
2632

src/internal/utils/date-time/__tests__/format-date-iso.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('formatDateISO', () => {
77
let formatTimeOffsetISOMock: jest.SpyInstance;
88

99
beforeEach(() => {
10-
formatTimeOffsetISOMock = jest.spyOn(formatTimeOffsetModule, 'formatTimeOffsetISO').mockReturnValue('+00:00');
10+
formatTimeOffsetISOMock = jest.spyOn(formatTimeOffsetModule, 'formatTimeOffsetISO').mockReturnValue('Z');
1111
});
1212

1313
afterEach(() => {
@@ -55,7 +55,7 @@ describe('formatDateISO', () => {
5555
isMonthOnly: false,
5656
});
5757

58-
expect(result).toBe('2023-06-15T12:00:00+00:00');
58+
expect(result).toBe('2023-06-15T12:00:00Z');
5959
expect(formatTimeOffsetISOMock).toHaveBeenCalledWith('2023-06-15T12:00:00', undefined);
6060
});
6161

src/internal/utils/date-time/__tests__/format-date-localized.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as formatTimeOffsetModule from '../format-time-offset';
66
describe('formatDateLocalized', () => {
77
beforeEach(() => {
88
jest.clearAllMocks();
9-
jest.spyOn(formatTimeOffsetModule, 'formatTimeOffsetLocalized').mockReturnValue('UTC+00:00');
9+
jest.spyOn(formatTimeOffsetModule, 'formatTimeOffsetLocalized').mockReturnValue('(UTC)');
1010
});
1111

1212
afterEach(() => {
@@ -32,7 +32,7 @@ describe('formatDateLocalized', () => {
3232
locale: 'en-US',
3333
});
3434

35-
expect(result).toMatch(/^June 15, 2023, 12:00:00 UTC\+00:00$/);
35+
expect(result).toMatch(/^June 15, 2023, 12:00:00 \(UTC\)$/);
3636
});
3737

3838
test('formats date only when isDateOnly is true', () => {
@@ -66,7 +66,7 @@ describe('formatDateLocalized', () => {
6666
locale: 'ja',
6767
});
6868

69-
expect(result).toMatch(/^2023615 12:00:00 UTC\+00:00$/);
69+
expect(result).toMatch(/^2023615 12:00:00 \(UTC\)$/);
7070
});
7171

7272
test('handles non-ISO formatted date strings', () => {
@@ -77,7 +77,7 @@ describe('formatDateLocalized', () => {
7777
locale: 'en-US',
7878
});
7979

80-
expect(result).toMatch(/^June 15, 2023, 12:00:00 UTC\+00:00$/);
80+
expect(result).toMatch(/^June 15, 2023, 12:00:00 \(UTC\)$/);
8181
});
8282

8383
//todo determine how to handle this failing

src/internal/utils/date-time/__tests__/format-date-time-with-offset.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ describe('formatDateTimeWithOffset', () => {
6363
date: '2020-01-01T00:00:00',
6464
timeOffset: regional,
6565
expected: {
66-
iso: '2020-01-01T00:00:00+00:00',
66+
iso: '2020-01-01T00:00:00Z',
6767
localized: { 'en-US': 'January 1, 2020, 00:00:00 (UTC)' },
6868
},
6969
},

src/internal/utils/date-time/__tests__/format-time-offset.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { formatTimeOffsetISO } from '../../../../../lib/components/internal/util
66
test('formatTimeOffsetISO', () => {
77
for (let offset = -120; offset <= 120; offset++) {
88
const formatted = formatTimeOffsetISO('2020-01-01', offset);
9+
if (offset === 0) {
10+
expect(formatted).toBe('Z');
11+
continue;
12+
}
13+
914
const sign = Number(formatted[0] + '1');
1015
const hours = Number(formatted[1] + formatted[2]);
1116
const minutes = Number(formatted[4] + formatted[5]);

src/internal/utils/date-time/format-time-offset.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,31 @@
33

44
import { padLeftZeros } from '../strings';
55

6+
/**
7+
* Formats timezone offset values used in for APIs, maintaining backward compatibility. Always
8+
* returns "+HH:MM" format, even for UTC ("+00:00"). Used by onChange events to preserve existing
9+
* API behavior.
10+
*/
11+
export function formatTimeOffsetISOInternal(isoDate: string, offsetInMinutes?: number) {
12+
offsetInMinutes = defaultToLocal(isoDate, offsetInMinutes);
13+
const { hours, minutes } = getMinutesAndHours(offsetInMinutes);
14+
15+
const sign = offsetInMinutes < 0 ? '-' : '+';
16+
const formattedOffset = `${sign}${formatISO2Digits(hours)}:${formatISO2Digits(minutes)}`;
17+
18+
return formattedOffset;
19+
}
20+
21+
/**
22+
* Formats timezone offset for display purposes using succinct UTC notation.
23+
* Returns "Z" for UTC, "+HH:MM" for other offsets.
24+
* Used for visual display in components like date-range-picker trigger text.
25+
*/
626
export function formatTimeOffsetISO(isoDate: string, offsetInMinutes?: number) {
727
offsetInMinutes = defaultToLocal(isoDate, offsetInMinutes);
28+
if (offsetInMinutes === 0) {
29+
return 'Z';
30+
}
831
const { hours, minutes } = getMinutesAndHours(offsetInMinutes);
932

1033
const sign = offsetInMinutes < 0 ? '-' : '+';

src/internal/utils/date-time/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export { formatDateTimeWithOffset } from './format-date-time-with-offset';
66
export { formatDate } from './format-date';
77
export { formatTime } from './format-time';
88
export { formatDateTime } from './format-date-time';
9-
export { formatTimeOffsetISO } from './format-time-offset';
9+
export { formatTimeOffsetISO, formatTimeOffsetISOInternal } from './format-time-offset';
1010
export { isIsoDateOnly, isIsoMonthOnly } from './is-iso-only';
1111
export { joinDateTime, splitDateTime } from './join-date-time';
1212
export { parseDate } from './parse-date';

0 commit comments

Comments
 (0)