Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date picker [LG-3894] Local date strings #2137

Merged
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
4 changes: 3 additions & 1 deletion packages/date-picker/src/DatePicker/DatePicker.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1244,7 +1244,9 @@ describe('packages/date-picker', () => {
/**
* Arrow Keys:
* Since arrow key behavior changes based on whether the input or menu is focused,
* many tests exist in the "DatePickerInput" and "DatePickerMenu" components
* more detailed tests suites are located in
* - DatePickerInput: (./DatePickerInput/DatePickerInput.spec.tsx) and
* - DatePickerMenu: (./DatePickerMenu/DatePickerMenu.spec.tsx)
*/
describe('Arrow key', () => {
describe('Input', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,6 @@ describe('packages/date-picker/date-picker-menu', () => {
);
});

describe('rendered cells', () => {
test('have correct `aria-label`', () => {
const { getCellWithISOString } = renderDatePickerMenu();
expect(getCellWithISOString('2023-09-10')).toHaveAttribute(
'aria-label',
'Sun Sep 10 2023',
);
});
});

describe('when value is updated', () => {
test('grid is labelled as the current month', () => {
const { getByRole, rerenderDatePickerMenu } = renderDatePickerMenu();
Expand Down Expand Up @@ -224,27 +214,34 @@ describe('packages/date-picker/date-picker-menu', () => {
});
});

// TODO: Test in multiple time zones with a properly mocked Date object
describe('rendered cells', () => {
test('have correct text content and `aria-label`', () => {
const { calendarCells } = renderDatePickerMenu();

calendarCells.forEach((cell, i) => {
const date = String(i + 1);
expect(cell).toHaveTextContent(date);

expect(cell).toHaveAttribute(
'aria-label',
expect.stringContaining(`September ${date}, 2023`),
);
});
});
});

describe.each(testTimeZones)(
'when system time is in $tz',
({ tz, UTCOffset }) => {
describe.each([
{ tz: undefined, UTCOffset: undefined },
...testTimeZones,
])('and timeZone prop is $tz', prop => {
const dec24Local = newUTC(
2023,
Month.December,
24,
23 - (prop.UTCOffset ?? UTCOffset),
59,
);
const dec25Local = newUTC(
2023,
Month.December,
25,
0 - (prop.UTCOffset ?? UTCOffset),
0,
);
const elevenLocal = 23 - (prop.UTCOffset ?? UTCOffset);
const midnightLocal = 0 - (prop.UTCOffset ?? UTCOffset);
const dec24Local = newUTC(2023, Month.December, 24, elevenLocal, 59);
const dec25Local = newUTC(2023, Month.December, 25, midnightLocal, 0);
const dec24ISO = '2023-12-24';
const dec25ISO = '2023-12-25';
const ctx = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getSimulatedTZDate } from './getSimulatedTZDate';

describe('packages/date-utils/getSimulatedTZDate', () => {
test.each(testTimeZones)('Simulates a date in $tz', ({ tz, UTCOffset }) => {
// 2023-07-04
// 2023-12-25
const testDate = newUTC(2023, Month.December, 25, 0, 0);

const sim = getSimulatedTZDate(testDate, tz);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getTimezoneOffset } from 'date-fns-tz';
import { isValidDate } from '../isValidDate';

/**
* Returns a date object that, _looks like_ the local time
* Returns a date object that _looks like_ the local time
* for the given time zone when printed in ISO format.
*
* e.g. given `date = "2023-12-25T01:00Z"` and `timeZone = "America/Los_Angeles"`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Month } from '../constants';
import { newUTC } from '../newUTC';

import { getSimulatedUTCDate } from '.';

describe('packages/date-utils/getSimulatedUTCDate', () => {
// TODO: Test in multiple time zones with properly mocked Date object
test('Simulates a date in UTC', () => {
// 2023-12-25
const testDate = newUTC(2023, Month.December, 25, 0, 0);

const sim = getSimulatedUTCDate(testDate);

expect(sim?.toDateString()).toEqual('Mon Dec 25 2023');
});
});
32 changes: 32 additions & 0 deletions packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { addMilliseconds } from 'date-fns';
import { getTimezoneOffset } from 'date-fns-tz';

import { isValidDate } from '../isValidDate';

/**
* The inverse of `getSimulatedTZDate`, returns a date object that _looks like_
* the UTC representation when printed in the local time zone.
*
* e.g. given `date = "2023-12-25T01:00Z"` and `timeZone = "America/Los_Angeles"`,
* by default using `date.toDateString` (or similar)
* this would print the locale string:
* "Sun Dec 24 2023"
*
* This function returns a modified, (technically incorrect) date object,
* such that the function `date.toDateString` (or similar)
* returns the locale string:
* `Mon Dec 25 2023`
*/
export const getSimulatedUTCDate = (date: Date, timeZone?: string): Date => {
timeZone = timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;

if (!isValidDate(date)) return date;

// Milliseconds offset between the given time zone & UTC
const offsetMs = getTimezoneOffset(timeZone, date);
if (isNaN(offsetMs)) return date;

const simulatedUTC = addMilliseconds(date, -offsetMs);

return simulatedUTC;
};
1 change: 1 addition & 0 deletions packages/date-utils/src/getSimulatedUTCDate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { getSimulatedUTCDate } from './getSimulatedUTCDate';
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { Month } from '../constants';
import { newUTC } from '../newUTC';

import { getUTCDateString } from '.';

describe('packages/date-utils/getUTCDateString', () => {
// TODO: Test in multiple time zones with a properly mocked Date object
test('returns date string relative to UTC', () => {
const date = new Date(Date.UTC(2023, Month.September, 10));
const date = newUTC(2023, Month.September, 10);
const str = getUTCDateString(date);
expect(str).toBe('Sun Sep 10 2023');
expect(str).toBe('Sunday, September 10, 2023');
});

test('returns date string relative with time provided', () => {
const date = new Date(Date.UTC(2023, Month.September, 10, 12, 0));
const str = getUTCDateString(date);
expect(str).toBe('Sun Sep 10 2023');
});
test.todo('returns a localized date string');
});
41 changes: 34 additions & 7 deletions packages/date-utils/src/getUTCDateString/getUTCDateString.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
import { addMinutes } from 'date-fns';
import { getSimulatedUTCDate } from '../getSimulatedUTCDate';
import { isValidLocale } from '../isValidLocale';

export const getUTCDateString = (date: Date): string => {
const utcOffsetMins = date.getTimezoneOffset();
interface GetUTCDateStringOptions {
locale?: string;
}

// Create a timestamp that, when printed in local time,
// appears as the UTC equivalent of the provided date
const fakeUTCDate = addMinutes(date, utcOffsetMins);
/**
* Returns a localized date string for the UTC representation of a date, regardless of system time zone
*
* e.g.
* ```
* getUTCDateString(
* Date("2023-12-25T01:00:00Z"),
* { locale: 'en-US' }
* ) // "Monday, December 25, 2023"
* ```
*/
export const getUTCDateString = (
date: Date,
options?: GetUTCDateStringOptions,
): string => {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

const dateInTZ = getSimulatedUTCDate(date, timeZone);

const locale = isValidLocale(options?.locale)
? options?.locale
: Intl.DateTimeFormat().resolvedOptions().locale;

const utcDateString = dateInTZ.toLocaleDateString(locale, {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
});

const utcDateString = fakeUTCDate.toDateString();
return utcDateString;
};
1 change: 1 addition & 0 deletions packages/date-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export { getLocaleWeekdays } from './getLocaleWeekdays';
export { getMonthIndex } from './getMonthIndex';
export { getMonthName } from './getMonthName';
export { getSimulatedTZDate } from './getSimulatedTZDate';
export { getSimulatedUTCDate } from './getSimulatedUTCDate';
export { getUTCDateString } from './getUTCDateString';
export { getWeekdayName } from './getWeekdayName';
export { getWeeksArray } from './getWeeksArray';
Expand Down
2 changes: 2 additions & 0 deletions packages/date-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ export type DateRangeType = [DateType, DateType];
export type LocaleString = 'iso8601' | string;

export interface MonthObject {
/** The localized long-form month name (e.g. December, July) */
long: string;
/** A localized short-form month name (e.g. Dec, Jul) */
short: string;
}

Expand Down