From 905f8f2c7f058e13a7fd0f1a877acf9413752f16 Mon Sep 17 00:00:00 2001 From: laliconfigcat Date: Thu, 23 Jul 2026 16:45:34 +0200 Subject: [PATCH] feat(material-luxon-adapter): add option to set zone Add zone option to be able to change the zone through MAT_LUXON_DATE_ADAPTER_OPTIONS. --- .../adapter/luxon-date-adapter.spec.ts | 94 ++++++++++++++++++- .../adapter/luxon-date-adapter.ts | 60 ++++++++---- src/material/datepicker/datepicker.md | 8 ++ 3 files changed, 141 insertions(+), 21 deletions(-) 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..98b04d52974e 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, @@ -760,12 +760,13 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override', () => let adapter: DateAdapter; beforeEach(() => { + // The zone parameter is specified to test that the useUtc parameter has precedence over the zone setting. TestBed.configureTestingModule({ imports: [LuxonDateModule], providers: [ { provide: MAT_LUXON_DATE_ADAPTER_OPTIONS, - useValue: {useUtc: true, firstDayOfWeek: 1}, + useValue: {useUtc: true, firstDayOfWeek: 1, zone: 'Europe/Budapest'}, }, ], }); @@ -789,7 +790,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()); }); @@ -835,6 +836,93 @@ describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS override for defa }); }); +describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS zone override', () => { + let adapter: DateAdapter; + + const zone = 'UTC-12'; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [ + { + provide: MAT_LUXON_DATE_ADAPTER_OPTIONS, + useValue: {firstDayOfWeek: 1, zone}, + }, + ], + }); + + adapter = TestBed.inject(DateAdapter); + }); + + describe(`use ${zone} zone`, () => { + it('should create Luxon date in specified zone', () => { + const date = adapter.createDate(2017, 0, 5); + expect(date.zone.name).toEqual(zone); + expect(date.toISO()).toEqual(DateTime.local(2017, JAN, 5, {zone}).toISO()); + }); + + it('should create today in specified zone', () => { + const today = adapter.today(); + expect(today.zone.name).toEqual(zone); + }); + + it('should parse dates to specified zone', () => { + const date = adapter.parse('1/2/2017', 'L/d/yyyy')!; + expect(date.zone.name).toEqual(zone); + expect(date.toISO()).toBe(DateTime.local(2017, JAN, 2, {zone}).toISO()); + }); + + it('should return date when deserializing in specified zone', () => { + const date = adapter.deserialize('1985-04-12T23:20:50.52Z')!; + expect(date.zone.name).toEqual(zone); + expect(date.toISO()).toBe('1985-04-12T11:20:50.520-12:00'); + }); + }); +}); + +describe('LuxonDateAdapter with MAT_LUXON_DATE_ADAPTER_OPTIONS zone override programmatically', () => { + let adapter: LuxonDateAdapter; + + const zone = 'UTC-12'; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LuxonDateModule], + providers: [ + { + provide: MAT_LUXON_DATE_ADAPTER_OPTIONS, + useValue: {firstDayOfWeek: 1, zone}, + }, + ], + }); + + adapter = TestBed.inject(DateAdapter) as LuxonDateAdapter; + }); + + describe(`use ${zone} zone`, () => { + it('should return correct zone after setting zone programmatically', () => { + const date = adapter.createDate(2017, 0, 5); + expect(date.zone.name).toEqual(zone); + + const newZone = 'UTC-6'; + adapter.setZone(newZone); + + const dateAfterZoneChange = adapter.createDate(2017, 0, 5); + expect(dateAfterZoneChange.zone.name).toEqual(newZone); + + const today = adapter.today(); + expect(today.zone.name).toEqual(newZone); + + const parsedDate = adapter.parse('1/2/2017', 'L/d/yyyy')!; + expect(parsedDate.zone.name).toEqual(newZone); + + const deserializedDate = adapter.deserialize('1985-04-12T23:20:50.52Z')!; + expect(deserializedDate.zone.name).toEqual(newZone); + }); + }); +}); + 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..c1eb34ac966f 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 zone of DateTime objects. + * Changing this will change how Angular Material components like DatePicker output dates. + * The zone parameter will be ignored if the useUtc parameter is set to true. + */ + zone?: 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 _zone?: 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.setZone(options?.zone); 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,31 @@ 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}); } - /** Gets the options that should be used when constructing a new `DateTime` object. */ - private _getOptions(): LuxonDateTimeOptions { + /** + * Sets the zone of DateTime objects. + * Changing this will change how Angular Material components like DatePicker output dates. + * The zone parameter will be ignored if the useUtc parameter is set to true. + */ + setZone(zone?: string) { + this._zone = zone; + } + + /** 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._zone, + }; + } } diff --git a/src/material/datepicker/datepicker.md b/src/material/datepicker/datepicker.md index a95f0af9852f..30d3ce4fe0ac 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 time zone by passing `zone` into `provideLuxonDateAdapter`: + +```ts +bootstrapApplication(MyApp, { + providers: [provideLuxonDateAdapter(undefined, {zone: '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