-
Notifications
You must be signed in to change notification settings - Fork 309
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NAS-132423 / 25.04 / Add ix-datepicker component (#11090)
--------- Co-authored-by: undsoft <[email protected]>
- Loading branch information
1 parent
9fe24d0
commit 485593d
Showing
132 changed files
with
669 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...at-date-time/format-datetime.pipe.spec.ts → ...at-date-time/format-datetime.pipe.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { NativeDateAdapter } from '@angular/material/core'; | ||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; | ||
import { FormatDateTimePipe } from 'app/modules/dates/pipes/format-date-time/format-datetime.pipe'; | ||
import { LocaleService } from 'app/services/locale.service'; | ||
import { IxDateAdapter } from './ix-date-adapter'; | ||
|
||
describe('IxDateAdapter', () => { | ||
let spectator: SpectatorService<IxDateAdapter>; | ||
const createService = createServiceFactory({ | ||
service: IxDateAdapter, | ||
mocks: [LocaleService, FormatDateTimePipe], | ||
providers: [ | ||
{ provide: NativeDateAdapter, useValue: {} }, | ||
], | ||
}); | ||
|
||
beforeEach(() => { | ||
spectator = createService(); | ||
}); | ||
|
||
describe('format', () => { | ||
it('should use FormatDateTimePipe for normal long dates', () => { | ||
const mockDate = new Date(2023, 10, 25); | ||
const formattedDate = '25/11/2023'; | ||
spectator.inject(FormatDateTimePipe).transform.mockReturnValue(formattedDate); | ||
|
||
const result = spectator.service.format(mockDate, { year: 'numeric', month: 'numeric', day: 'numeric' }); | ||
|
||
expect(spectator.inject(FormatDateTimePipe).transform).toHaveBeenCalledWith(mockDate, null, ' '); | ||
expect(result).toBe(formattedDate); | ||
}); | ||
|
||
it('should fallback to super.format when short date is requested ("day" is not in the format)', () => { | ||
const mockDate = new Date(2023, 10, 25); | ||
jest.spyOn(NativeDateAdapter.prototype, 'format').mockReturnValue('11/2023'); | ||
|
||
const result = spectator.service.format(mockDate, { year: 'numeric', month: 'numeric' }); | ||
|
||
expect(result).toBe('11/2023'); | ||
expect(NativeDateAdapter.prototype.format).toHaveBeenCalledWith(mockDate, { year: 'numeric', month: 'numeric' }); | ||
}); | ||
}); | ||
|
||
describe('parse', () => { | ||
it('should return null if the value is not a string or is empty', () => { | ||
expect(spectator.service.parse('')).toBeNull(); | ||
expect(spectator.service.parse(null)).toBeNull(); | ||
expect(spectator.service.parse(undefined)).toBeNull(); | ||
}); | ||
|
||
it('should use LocaleService to parse valid string values', () => { | ||
const dateString = '2023-11-25'; | ||
const mockDate = new Date(2023, 10, 25); | ||
spectator.inject(LocaleService).getDateFromString.mockReturnValue(mockDate); | ||
|
||
const result = spectator.service.parse(dateString); | ||
|
||
expect(spectator.inject(LocaleService).getDateFromString).toHaveBeenCalledWith(dateString); | ||
expect(result).toBe(mockDate); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { NativeDateAdapter } from '@angular/material/core'; | ||
import { FormatDateTimePipe } from 'app/modules/dates/pipes/format-date-time/format-datetime.pipe'; | ||
import { LocaleService } from 'app/services/locale.service'; | ||
|
||
/** | ||
* This is to be provided in components when we need to format and parse a date according | ||
* to the time format user has selected in the preferences. | ||
* | ||
* TODO: It may be better to provide MAT_DATE_FORMATS and use provideDateFnsAdapter instead. | ||
*/ | ||
// eslint-disable-next-line angular-file-naming/service-filename-suffix | ||
@Injectable() | ||
export class IxDateAdapter extends NativeDateAdapter { | ||
constructor( | ||
private localeService: LocaleService, | ||
private formatDateTime: FormatDateTimePipe, | ||
) { | ||
super(); | ||
} | ||
|
||
override format(date: Date, format: { year: string; month: string; day?: string }): string { | ||
if (!('day' in format)) { | ||
return super.format(date, format); | ||
} | ||
// TODO: Pipe does not support disabling time formatting properly. | ||
return this.formatDateTime.transform(date, null, ' '); | ||
} | ||
|
||
override parse(value: unknown, _?: unknown): Date | null { | ||
if (typeof value !== 'string' || !value) { | ||
return super.parse(value); | ||
} | ||
|
||
return this.localeService.getDateFromString(value); | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
src/app/modules/forms/ix-forms/components/ix-date-picker/ix-date-picker.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
@if (label() || tooltip()) { | ||
<ix-label | ||
[label]="label()" | ||
[tooltip]="tooltip()" | ||
[required]="required()" | ||
[ixTestOverride]="controlDirective.name" | ||
></ix-label> | ||
} | ||
|
||
<div class="input-container" ixRegisteredControl> | ||
<mat-datepicker #datepicker></mat-datepicker> | ||
<input | ||
matInput | ||
[value]="value()" | ||
[placeholder]="placeholder()" | ||
[matDatepicker]="datepicker" | ||
[min]="min()" | ||
[max]="max()" | ||
[ixTest]="controlDirective.name" | ||
[attr.aria-label]="label()" | ||
[readonly]="readonly()" | ||
[disabled]="isDisabled()" | ||
(dateChange)="onDateChanged($event)" | ||
(focus)="datepicker.open()" | ||
(blur)="blurred()" | ||
> | ||
<mat-datepicker-toggle matIconSuffix [for]="datepicker"></mat-datepicker-toggle> | ||
</div> | ||
|
||
<ix-errors [control]="controlDirective.control" [label]="label()"></ix-errors> | ||
|
||
@if (hint()) { | ||
<mat-hint>{{ hint() }}</mat-hint> | ||
} |
33 changes: 33 additions & 0 deletions
33
src/app/modules/forms/ix-forms/components/ix-date-picker/ix-date-picker.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
:host { | ||
display: block; | ||
margin-bottom: 12px; | ||
} | ||
|
||
.input-container { | ||
background: var(--bg1); | ||
border: solid 1.5px var(--lines); | ||
border-radius: 2px; | ||
display: flex; | ||
flex-direction: row; | ||
font-size: 12px; | ||
position: relative; | ||
|
||
&:focus-within { | ||
border-color: var(--primary); | ||
} | ||
|
||
input { | ||
background: none; | ||
border: 0; | ||
color: var(--fg1); | ||
width: 100%; | ||
|
||
&:focus { | ||
outline: none; | ||
} | ||
} | ||
|
||
::ng-deep mat-datepicker-toggle svg { | ||
width: 18px; | ||
} | ||
} |
159 changes: 159 additions & 0 deletions
159
src/app/modules/forms/ix-forms/components/ix-date-picker/ix-date-picker.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import { HarnessLoader } from '@angular/cdk/testing'; | ||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; | ||
import { FormControl, ReactiveFormsModule } from '@angular/forms'; | ||
import { DateAdapter } from '@angular/material/core'; | ||
import { MatDatepickerModule } from '@angular/material/datepicker'; | ||
import { MatDatepickerInputHarness } from '@angular/material/datepicker/testing'; | ||
import { MatInputModule } from '@angular/material/input'; | ||
import { MatInputHarness } from '@angular/material/input/testing'; | ||
import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest'; | ||
import { parseISO } from 'date-fns'; | ||
import { MockComponent } from 'ng-mocks'; | ||
import { FormatDateTimePipe } from 'app/modules/dates/pipes/format-date-time/format-datetime.pipe'; | ||
import { IxDateAdapter } from 'app/modules/dates/services/ix-date-adapter'; | ||
import { IxErrorsComponent } from 'app/modules/forms/ix-forms/components/ix-errors/ix-errors.component'; | ||
import { IxLabelComponent } from 'app/modules/forms/ix-forms/components/ix-label/ix-label.component'; | ||
import { IxIconComponent } from 'app/modules/ix-icon/ix-icon.component'; | ||
import { LocaleService } from 'app/services/locale.service'; | ||
import { IxDatepickerComponent } from './ix-date-picker.component'; | ||
|
||
describe('IxDatePickerComponent', () => { | ||
let spectator: SpectatorHost<IxDatepickerComponent>; | ||
let loader: HarnessLoader; | ||
const formControl = new FormControl<Date>(null); | ||
|
||
const createHost = createHostFactory({ | ||
component: IxDatepickerComponent, | ||
imports: [ | ||
ReactiveFormsModule, | ||
MatDatepickerModule, | ||
MatInputModule, | ||
], | ||
declarations: [ | ||
MockComponent(IxErrorsComponent), | ||
MockComponent(IxLabelComponent), | ||
MockComponent(IxIconComponent), | ||
], | ||
providers: [ | ||
mockProvider(LocaleService, { | ||
// User timezone is UTC+2, as set in jest config. | ||
// Machine timezone is UTC+1, resulting machine timezone being -1 compared to user timezone. | ||
timezone: 'Europe/Berlin', | ||
}), | ||
], | ||
componentProviders: [ | ||
IxDateAdapter, | ||
FormatDateTimePipe, | ||
{ | ||
provide: DateAdapter, | ||
deps: [IxDateAdapter], | ||
useFactory: (dateAdapter: IxDateAdapter) => { | ||
jest.spyOn(dateAdapter, 'format').mockImplementation(() => 'January 1st, 2021'); | ||
jest.spyOn(dateAdapter, 'parse').mockImplementation(() => new Date(2021, 0, 2, 0, 0, 0)); | ||
return dateAdapter; | ||
}, | ||
}, | ||
], | ||
}); | ||
|
||
describe('rendering', () => { | ||
it('shows label with form label, tooltip and required values', () => { | ||
spectator = createHost(`<ix-datepicker | ||
label="Label" | ||
tooltip="Tooltip" | ||
[required]="true" | ||
[formControl]="formControl" | ||
></ix-datepicker>`, { | ||
hostProps: { | ||
formControl, | ||
}, | ||
}); | ||
|
||
const label = spectator.query(IxLabelComponent); | ||
expect(label).toExist(); | ||
expect(label.label).toBe('Label'); | ||
expect(label.tooltip).toBe('Tooltip'); | ||
expect(label.required).toBe(true); | ||
}); | ||
|
||
it('opens datepicker when input is clicked', async () => { | ||
spectator = createHost('<ix-datepicker [formControl]="formControl"></ix-datepicker>', { | ||
hostProps: { | ||
formControl, | ||
}, | ||
}); | ||
loader = TestbedHarnessEnvironment.loader(spectator.fixture); | ||
|
||
const input = await loader.getHarness(MatInputHarness); | ||
await input.focus(); | ||
|
||
const datepicker = await loader.getHarness(MatDatepickerInputHarness); | ||
expect(await datepicker.isCalendarOpen()).toBe(true); | ||
}); | ||
|
||
it('passes min and max in browser timezone params to mat-datepicker', async () => { | ||
spectator = createHost('<ix-datepicker [formControl]="formControl" [min]="min" [max]="max"></ix-datepicker>', { | ||
hostProps: { | ||
formControl, | ||
min: new Date(2020, 0, 1, 12, 0, 0), | ||
max: new Date(2020, 0, 2, 12, 0, 0), | ||
}, | ||
}); | ||
loader = TestbedHarnessEnvironment.loader(spectator.fixture); | ||
|
||
const datepicker = await loader.getHarness(MatDatepickerInputHarness); | ||
expect(await datepicker.getMin()).toBe('2020-01-01'); | ||
expect(await datepicker.getMax()).toBe('2020-01-02'); | ||
}); | ||
}); | ||
|
||
describe('form control', () => { | ||
it('shows form control value in browser timezone in the input', async () => { | ||
spectator = createHost('<ix-datepicker [formControl]="formControl"></ix-datepicker>', { | ||
hostProps: { | ||
formControl, | ||
}, | ||
}); | ||
loader = TestbedHarnessEnvironment.loader(spectator.fixture); | ||
|
||
formControl.setValue(parseISO('2021-01-01T23:00:00+01:00')); | ||
|
||
const input = await loader.getHarness(MatInputHarness); | ||
|
||
expect(await input.getValue()).toMatch('January 1st, 2021'); | ||
}); | ||
|
||
it('updates form control with date in machine timezone when user types in new date', async () => { | ||
spectator = createHost('<ix-datepicker [formControl]="formControl"></ix-datepicker>', { | ||
hostProps: { | ||
formControl, | ||
}, | ||
}); | ||
loader = TestbedHarnessEnvironment.loader(spectator.fixture); | ||
|
||
const datepicker = await loader.getHarness(MatDatepickerInputHarness); | ||
await datepicker.setValue('January 2nd, 2021'); | ||
|
||
expect(formControl.value).toEqual(parseISO('2021-01-01T23:00:00Z')); | ||
}); | ||
|
||
it('updates form control with date in machine timezone when user selects new date in datepicker', async () => { | ||
spectator = createHost('<ix-datepicker [formControl]="formControl"></ix-datepicker>', { | ||
hostProps: { | ||
formControl, | ||
}, | ||
}); | ||
loader = TestbedHarnessEnvironment.loader(spectator.fixture); | ||
|
||
const datepicker = await loader.getHarness(MatDatepickerInputHarness); | ||
await datepicker.openCalendar(); | ||
const calendar = await datepicker.getCalendar(); | ||
await calendar.changeView(); // Switch to years | ||
await calendar.selectCell({ text: '2024' }); | ||
await calendar.selectCell({ text: 'JAN' }); | ||
await calendar.selectCell({ text: '4' }); | ||
|
||
expect(formControl.value).toEqual(parseISO('2024-01-03T23:00:00.000Z')); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.