diff --git a/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts b/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts index a8873f36afd2..83f81277e175 100644 --- a/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts +++ b/src/material-luxon-adapter/adapter/luxon-date-adapter.spec.ts @@ -11,7 +11,7 @@ import {TestBed} from '@angular/core/testing'; import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material/core'; import {CalendarSystem, DateTime, FixedOffsetZone, Settings} from 'luxon'; import {LuxonDateModule} from './index'; -import {MAT_LUXON_DATE_ADAPTER_OPTIONS} from './luxon-date-adapter'; +import {LuxonDateAdapter, MAT_LUXON_DATE_ADAPTER_OPTIONS} from './luxon-date-adapter'; const JAN = 1, FEB = 2, @@ -418,7 +418,7 @@ describe('LuxonDateAdapter', () => { expect(clone.toISO()).toEqual(date.toISO()); }); - it('should respect timezone on clone', () => { + it('should respect timeZone on clone', () => { const dateLocal = DateTime.local(2017, JAN, 1); const dateInCet = dateLocal.setZone('Europe/Budapest'); const cloneInCet = adapter.clone(dateInCet); @@ -757,7 +757,7 @@ describe('LuxonDateAdapter with LOCALE_ID override', () => { }); describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () => { - let adapter: DateAdapter; + let adapter: LuxonDateAdapter; beforeEach(() => { TestBed.configureTestingModule({ @@ -770,7 +770,7 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () => ], }); - adapter = TestBed.inject(DateAdapter); + adapter = TestBed.inject(DateAdapter) as LuxonDateAdapter; }); describe('use UTC', () => { @@ -789,7 +789,7 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () => }); it('should parse dates to UTC', () => { - const date = adapter.parse('1/2/2017', 'LL/dd/yyyy')!; + const date = adapter.parse('1/2/2017', 'L/d/yyyy')!; expect(date.toISO()).toBe(date.toUTC().toISO()); }); @@ -797,6 +797,30 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () => const date = adapter.deserialize('1985-04-12T23:20:50.52Z')!; expect(date.toISO()).toBe(date.toUTC().toISO()); }); + + it('setting timeZone should throw an error when useUtc is true', () => { + expect(() => adapter.setTimeZone('Europe/Budapest')).toThrowError( + 'Cannot set timeZone if the useUtc option is set to true.', + ); + }); + }); +}); + +describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS useUtc and timeZone override same time', () => { + it('setting useUtc and timeZone same time should throw an Error', () => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [ + { + provide: MAT_LUXON_DATE_ADAPTER_OPTIONS, + useValue: {useUtc: true, firstDayOfWeek: 1, timeZone: 'Europe/Budapest'}, + }, + ], + }); + + expect(() => TestBed.inject(DateAdapter)).toThrowError( + 'Cannot set timeZone if the useUtc option is set to true.', + ); }); }); @@ -835,6 +859,93 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override for defa }); }); +describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS timeZone override', () => { + let adapter: DateAdapter; + + const timeZone = 'UTC-12'; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [ + { + provide: MAT_LUXON_DATE_ADAPTER_OPTIONS, + useValue: {firstDayOfWeek: 1, timeZone}, + }, + ], + }); + + adapter = TestBed.inject(DateAdapter); + }); + + describe(`use ${timeZone} timeZone`, () => { + it('should create Luxon date in specified timeZone', () => { + const date = adapter.createDate(2017, 0, 5); + expect(date.zone.name).toEqual(timeZone); + expect(date.toISO()).toEqual(DateTime.local(2017, JAN, 5, {zone: timeZone}).toISO()); + }); + + it('should create today in specified timeZone', () => { + const today = adapter.today(); + expect(today.zone.name).toEqual(timeZone); + }); + + it('should parse dates to specified timeZone', () => { + const date = adapter.parse('1/2/2017', 'L/d/yyyy')!; + expect(date.zone.name).toEqual(timeZone); + expect(date.toISO()).toBe(DateTime.local(2017, JAN, 2, {zone: timeZone}).toISO()); + }); + + it('should return date when deserializing in specified timeZone', () => { + const date = adapter.deserialize('1985-04-12T23:20:50.52Z')!; + expect(date.zone.name).toEqual(timeZone); + expect(date.toISO()).toBe('1985-04-12T11:20:50.520-12:00'); + }); + }); +}); + +describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS timeZone override programmatically', () => { + let adapter: LuxonDateAdapter; + + const timeZone = 'UTC-12'; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [ + { + provide: MAT_LUXON_DATE_ADAPTER_OPTIONS, + useValue: {firstDayOfWeek: 1, timeZone}, + }, + ], + }); + + adapter = TestBed.inject(DateAdapter) as LuxonDateAdapter; + }); + + describe(`use ${timeZone} timeZone`, () => { + it('should return correct timeZone after setting zone programmatically', () => { + const date = adapter.createDate(2017, 0, 5); + expect(date.zone.name).toEqual(timeZone); + + const newTimeZone = 'UTC-6'; + adapter.setTimeZone(newTimeZone); + + const dateAfterZoneChange = adapter.createDate(2017, 0, 5); + expect(dateAfterZoneChange.zone.name).toEqual(newTimeZone); + + const today = adapter.today(); + expect(today.zone.name).toEqual(newTimeZone); + + const parsedDate = adapter.parse('1/2/2017', 'L/d/yyyy')!; + expect(parsedDate.zone.name).toEqual(newTimeZone); + + const deserializedDate = adapter.deserialize('1985-04-12T23:20:50.52Z')!; + expect(deserializedDate.zone.name).toEqual(newTimeZone); + }); + }); +}); + function assertValidDate(adapter: DateAdapter, d: DateTime | null, valid: boolean) { expect(adapter.isDateInstance(d)) .not.withContext(`Expected ${d} to be a date instance`) diff --git a/src/material-luxon-adapter/adapter/luxon-date-adapter.ts b/src/material-luxon-adapter/adapter/luxon-date-adapter.ts index 4afd606ac727..4a2fc802a20e 100644 --- a/src/material-luxon-adapter/adapter/luxon-date-adapter.ts +++ b/src/material-luxon-adapter/adapter/luxon-date-adapter.ts @@ -12,6 +12,7 @@ import { DateTime as LuxonDateTime, Info as LuxonInfo, DateTimeOptions as LuxonDateTimeOptions, + LocaleOptions as LuxonLocaleOptions, CalendarSystem as LuxonCalendarSystem, } from 'luxon'; @@ -23,6 +24,13 @@ export interface MatLuxonDateAdapterOptions { */ useUtc: boolean; + /** + * Sets the timeZone of DateTime objects. + * Changing this will change how Angular Material components like DatePicker output dates. + * Throws an error if the useUtc parameter is set to true and the timeZone is also provided. + */ + timeZone?: string; + /** * Sets the first day of week. * Changing this will change how Angular Material components like DatePicker shows start of week. @@ -63,6 +71,7 @@ export class LuxonDateAdapter extends DateAdapter { private _useUTC: boolean; private _firstDayOfWeek: number | undefined; private _defaultOutputCalendar: LuxonCalendarSystem; + private _timeZone?: string; constructor() { super(); @@ -75,6 +84,7 @@ export class LuxonDateAdapter extends DateAdapter { this._useUTC = !!options?.useUtc; this._firstDayOfWeek = options?.firstDayOfWeek; this._defaultOutputCalendar = options?.defaultOutputCalendar || 'gregory'; + this.setTimeZone(options?.timeZone); this.setLocale(dateLocale || LuxonDateTime.local().locale); } @@ -122,7 +132,7 @@ export class LuxonDateAdapter extends DateAdapter { } getYearName(date: LuxonDateTime): string { - return date.toFormat('yyyy', this._getOptions()); + return date.toFormat('yyyy', this._getLocaleOptions()); } getFirstDayOfWeek(): number { @@ -135,14 +145,12 @@ export class LuxonDateAdapter extends DateAdapter { clone(date: LuxonDateTime): LuxonDateTime { return LuxonDateTime.fromObject(date.toObject(), { - ...this._getOptions(), + ...this._getLocaleOptions(), zone: date.zone, }); } createDate(year: number, month: number, date: number): LuxonDateTime { - const options = this._getOptions(); - if (month < 0 || month > 11) { throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`); } @@ -153,8 +161,8 @@ export class LuxonDateAdapter extends DateAdapter { // Luxon uses 1-indexed months so we need to add one to the month. const result = this._useUTC - ? LuxonDateTime.utc(year, month + 1, date, options) - : LuxonDateTime.local(year, month + 1, date, options); + ? LuxonDateTime.utc(year, month + 1, date, this._getLocaleOptions()) + : LuxonDateTime.local(year, month + 1, date, this._getDateTimeOptions()); if (!this.isValid(result)) { throw Error(`Invalid date "${date}". Reason: "${result.invalidReason}".`); @@ -164,13 +172,13 @@ export class LuxonDateAdapter extends DateAdapter { } today(): LuxonDateTime { - const options = this._getOptions(); - - return this._useUTC ? LuxonDateTime.utc(options) : LuxonDateTime.local(options); + return this._useUTC + ? LuxonDateTime.utc(this._getLocaleOptions()) + : LuxonDateTime.local(this._getDateTimeOptions()); } parse(value: unknown, parseFormat: string | string[]): LuxonDateTime | null { - const options: LuxonDateTimeOptions = this._getOptions(); + const options: LuxonDateTimeOptions = this._getDateTimeOptions(); if (typeof value == 'string' && value.length > 0) { const iso8601Date = LuxonDateTime.fromISO(value, options); @@ -217,15 +225,15 @@ export class LuxonDateAdapter extends DateAdapter { } addCalendarYears(date: LuxonDateTime, years: number): LuxonDateTime { - return date.reconfigure(this._getOptions()).plus({years}); + return date.reconfigure(this._getLocaleOptions()).plus({years}); } addCalendarMonths(date: LuxonDateTime, months: number): LuxonDateTime { - return date.reconfigure(this._getOptions()).plus({months}); + return date.reconfigure(this._getLocaleOptions()).plus({months}); } addCalendarDays(date: LuxonDateTime, days: number): LuxonDateTime { - return date.reconfigure(this._getOptions()).plus({days}); + return date.reconfigure(this._getLocaleOptions()).plus({days}); } toIso8601(date: LuxonDateTime): string { @@ -238,7 +246,7 @@ export class LuxonDateAdapter extends DateAdapter { * string into null. Returns an invalid date for all other values. */ override deserialize(value: unknown): LuxonDateTime | null { - const options = this._getOptions(); + const options = this._getDateTimeOptions(); let date: LuxonDateTime | undefined; if (value instanceof Date) { date = LuxonDateTime.fromJSDate(value, options); @@ -320,15 +328,34 @@ export class LuxonDateAdapter extends DateAdapter { } override addSeconds(date: LuxonDateTime, amount: number): LuxonDateTime { - return date.reconfigure(this._getOptions()).plus({seconds: amount}); + return date.reconfigure(this._getLocaleOptions()).plus({seconds: amount}); + } + + /** + * Sets the timeZone of DateTime objects. + * Changing this will change how Angular Material components like DatePicker output dates. + * Throws an error if the useUtc parameter is set to true and the timeZone is also provided. + */ + setTimeZone(timeZone?: string) { + if (this._useUTC && timeZone) { + throw Error(`Cannot set timeZone if the useUtc option is set to true.`); + } + this._timeZone = timeZone; } - /** Gets the options that should be used when constructing a new `DateTime` object. */ - private _getOptions(): LuxonDateTimeOptions { + /** Gets the Locale options that should be used when Luxon expects a LocaleOptions parameter. */ + private _getLocaleOptions(): LuxonLocaleOptions { return { - zone: this._useUTC ? 'utc' : undefined, locale: this.locale, outputCalendar: this._defaultOutputCalendar, }; } + + /** Gets the DateTime options that should be used when Luxon expects a DateTimeOptions, e.g. when constructing/parsing a `DateTime` object. */ + private _getDateTimeOptions(): LuxonDateTimeOptions { + return { + ...this._getLocaleOptions(), + zone: this._useUTC ? 'utc' : this._timeZone, + }; + } } diff --git a/src/material/datepicker/datepicker.md b/src/material/datepicker/datepicker.md index a95f0af9852f..d44c298a0e80 100644 --- a/src/material/datepicker/datepicker.md +++ b/src/material/datepicker/datepicker.md @@ -430,6 +430,14 @@ bootstrapApplication(MyApp, { }); ``` +You can also change the default behaviour to parse and handle dates as any desired timeZone by passing `timeZone` into `provideLuxonDateAdapter`: + +```ts +bootstrapApplication(MyApp, { + providers: [provideLuxonDateAdapter(undefined, {timeZone: 'UTC+2'})] +}); +``` + It is also possible to create your own `DateAdapter` that works with any date format your app requires. This is accomplished by subclassing `DateAdapter` and providing your subclass as the `DateAdapter` implementation. You will also want to make sure that the `MAT_DATE_FORMATS` provided