From b6d7e1c75740a61dcd02c21692e4d4632fb8df5c Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Wed, 23 Feb 2022 08:42:51 -0500 Subject: [PATCH] fix(datetime): month picker no longer gives duplicate months on ios 14 and older (#24792) resolves #24663 --- .../components/datetime/test/format.spec.ts | 23 ++++++++++++- core/src/components/datetime/utils/data.ts | 33 ++++++++++++++++--- core/src/components/datetime/utils/format.ts | 12 +++---- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/core/src/components/datetime/test/format.spec.ts b/core/src/components/datetime/test/format.spec.ts index f1ea01d00da..cbbc940a0d9 100644 --- a/core/src/components/datetime/test/format.spec.ts +++ b/core/src/components/datetime/test/format.spec.ts @@ -27,6 +27,11 @@ describe('generateDayAriaLabel()', () => { expect(generateDayAriaLabel('en-US', false, reference)).toEqual('Monday, May 31'); }); + it('should return Saturday, April 1', () => { + const reference = { month: 4, day: 1, year: 2006 }; + + expect(generateDayAriaLabel('en-US', false, reference)).toEqual('Saturday, April 1'); + }); }); describe('getMonthAndDay()', () => { @@ -37,6 +42,14 @@ describe('getMonthAndDay()', () => { it('should return mar, 11 may', () => { expect(getMonthAndDay('es-ES', { month: 5, day: 11, year: 2021 })).toEqual('mar, 11 may'); }); + + it('should return Sat, Apr 1', () => { + expect(getMonthAndDay('en-US', { month: 4, day: 1, year: 2006 })).toEqual('Sat, Apr 1'); + }); + + it('should return sáb, 1 abr', () => { + expect(getMonthAndDay('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('sáb, 1 abr'); + }); }) describe('getFormattedHour()', () => { @@ -63,7 +76,15 @@ describe('getMonthAndYear()', () => { expect(getMonthAndYear('en-US', { month: 5, day: 11, year: 2021 })).toEqual('May 2021'); }); - it('should return mar, 11 may', () => { + it('should return mayo de 2021', () => { expect(getMonthAndYear('es-ES', { month: 5, day: 11, year: 2021 })).toEqual('mayo de 2021'); }); + + it('should return April 2006', () => { + expect(getMonthAndYear('en-US', { month: 4, day: 1, year: 2006 })).toEqual('April 2006'); + }); + + it('should return abril de 2006', () => { + expect(getMonthAndYear('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('abril de 2006'); + }); }) diff --git a/core/src/components/datetime/utils/data.ts b/core/src/components/datetime/utils/data.ts index 4006ee56dae..44015cd25ac 100644 --- a/core/src/components/datetime/utils/data.ts +++ b/core/src/components/datetime/utils/data.ts @@ -282,9 +282,9 @@ export const getPickerMonths = ( } processedMonths.forEach(processedMonth => { - const date = new Date(`${processedMonth}/1/${year}`); + const date = new Date(`${processedMonth}/1/${year} GMT+0000`); - const monthString = new Intl.DateTimeFormat(locale, { month: 'long' }).format(date); + const monthString = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' }).format(date); months.push({ text: monthString, value: processedMonth }); }); } else { @@ -292,9 +292,34 @@ export const getPickerMonths = ( const minMonth = minParts && minParts.year === year ? minParts.month : 1; for (let i = minMonth; i <= maxMonth; i++) { - const date = new Date(`${i}/1/${year}`); - const monthString = new Intl.DateTimeFormat(locale, { month: 'long' }).format(date); + /** + * + * There is a bug on iOS 14 where + * Intl.DateTimeFormat takes into account + * the local timezone offset when formatting dates. + * + * Forcing the timezone to 'UTC' fixes the issue. However, + * we should keep this workaround as it is safer. In the event + * this breaks in another browser, we will not be impacted + * because all dates will be interpreted in UTC. + * + * Example: + * new Intl.DateTimeFormat('en-US', { month: 'long' }).format(new Date('Sat Apr 01 2006 00:00:00 GMT-0400 (EDT)')) // "March" + * new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' }).format(new Date('Sat Apr 01 2006 00:00:00 GMT-0400 (EDT)')) // "April" + * + * In certain timezones, iOS 14 shows the wrong + * date for .toUTCString(). To combat this, we + * force all of the timezones to GMT+0000 (UTC). + * + * Example: + * Time Zone: Central European Standard Time + * new Date('1/1/1992').toUTCString() // "Tue, 31 Dec 1991 23:00:00 GMT" + * new Date('1/1/1992 GMT+0000').toUTCString() // "Wed, 01 Jan 1992 00:00:00 GMT" + */ + const date = new Date(`${i}/1/${year} GMT+0000`); + + const monthString = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' }).format(date); months.push({ text: monthString, value: i }); } } diff --git a/core/src/components/datetime/utils/format.ts b/core/src/components/datetime/utils/format.ts index f5a027b12bf..0762a7dacfb 100644 --- a/core/src/components/datetime/utils/format.ts +++ b/core/src/components/datetime/utils/format.ts @@ -56,9 +56,9 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D /** * MM/DD/YYYY will return midnight in the user's timezone. */ - const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}`); + const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`); - const labelString = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric' }).format(date); + const labelString = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(date); /** * If date is today, prepend "Today" so screen readers indicate @@ -72,8 +72,8 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D * Used for the header in MD mode. */ export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => { - const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}`); - return new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(date); + const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`); + return new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }).format(date); } /** @@ -83,6 +83,6 @@ export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => { * Example: May 2021 */ export const getMonthAndYear = (locale: string, refParts: DatetimeParts) => { - const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}`); - return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(date); + const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`); + return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date); }