Skip to content

Commit

Permalink
feat(core): dayPeriod pattern support (#12354)
Browse files Browse the repository at this point in the history
* feat(core): add dayPeriod pattern support

* refactor(core, docs): update Date/timePicker examples, optimized translateDayPeriod pipe performance, managed adapter format method to handle dayPeriod pattern etc...

* fix(core): simplified pipe if conditional check and updated time picker format example comment and updated dayPeriod strings

* fix(core): remove dayperiod strings from fr lang

* fix(core): handle invalid date inputs gracefully

* fix(docs): updated dateTime example with increased width of picker input

* fix(docs): timepicker format test

* fix(docs): dateTimePicker test

---------

Co-authored-by: deno <[email protected]>
Co-authored-by: Mike O'Donnell <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 08ef8f5 commit c70f83a
Show file tree
Hide file tree
Showing 22 changed files with 365 additions and 47 deletions.
3 changes: 2 additions & 1 deletion libs/core/datetime-picker/datetime-picker.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[disabled]="disabled"
[placement]="placement"
[appendTo]="appendTo"
[class.fd-popover-full-width]="isFullWidth"
>
<fd-popover-control>
<ng-template [ngTemplateOutlet]="controlTemplate"></ng-template>
Expand Down Expand Up @@ -50,7 +51,7 @@
[attr.aria-required]="required"
[placeholder]="placeholder"
[disabled]="disabled"
[ngModel]="_inputFieldDate"
[ngModel]="_inputFieldDate | translateDayPeriod"
(keyup.enter)="handleInputChange($any($event.target).value, false)"
(ngModelChange)="handleInputChange($event, true)"
(blur)="handleOnTouched($event)"
Expand Down
4 changes: 4 additions & 0 deletions libs/core/datetime-picker/datetime-picker.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ $block: fd-datetime;
}
}

.fd-popover-full-width {
width: 100%;
}

@media (max-width: 60rem) {
.fd-datetime {
&__display-type-switcher {
Expand Down
31 changes: 23 additions & 8 deletions libs/core/datetime-picker/datetime-picker.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,29 @@ import {
FdCalendarView,
FdCalendarViewEnum
} from '@fundamental-ngx/core/calendar';
import { DATE_TIME_FORMATS, DatetimeAdapter, DateTimeFormats } from '@fundamental-ngx/core/datetime';
import { FormItemControl, PopoverFormMessageService, registerFormItemControl } from '@fundamental-ngx/core/form';
import { InputGroupInputDirective } from '@fundamental-ngx/core/input-group';
import { PopoverService } from '@fundamental-ngx/core/popover';
import {
convertToDesiredFormat,
DATE_TIME_FORMATS,
DatetimeAdapter,
DateTimeFormats,
TranslateDayPeriodPipe
} from '@fundamental-ngx/core/datetime';
import {
FormItemControl,
FormMessageComponent,
PopoverFormMessageService,
registerFormItemControl
} from '@fundamental-ngx/core/form';
import { InputGroupInputDirective, InputGroupModule } from '@fundamental-ngx/core/input-group';
import { PopoverModule, PopoverService } from '@fundamental-ngx/core/popover';
import { Placement, SpecialDayRule } from '@fundamental-ngx/core/shared';

import { NgClass, NgTemplateOutlet } from '@angular/common';
import { FormStates } from '@fundamental-ngx/cdk/forms';
import { DynamicComponentService, FocusTrapService, Nullable } from '@fundamental-ngx/cdk/utils';
import { BarModule } from '@fundamental-ngx/core/bar';
import { ButtonComponent } from '@fundamental-ngx/core/button';
import { FormMessageComponent } from '@fundamental-ngx/core/form';
import { InputGroupModule } from '@fundamental-ngx/core/input-group';
import { MobileModeConfig } from '@fundamental-ngx/core/mobile-mode';
import { PopoverModule } from '@fundamental-ngx/core/popover';
import { SegmentedButtonComponent } from '@fundamental-ngx/core/segmented-button';
import { TimeModule } from '@fundamental-ngx/core/time';
import { FdTranslatePipe } from '@fundamental-ngx/i18n';
Expand Down Expand Up @@ -105,7 +113,8 @@ import { FD_DATETIME_PICKER_COMPONENT, FD_DATETIME_PICKER_MOBILE_CONFIG } from '
NgClass,
TimeModule,
BarModule,
FdTranslatePipe
FdTranslatePipe,
TranslateDayPeriodPipe
]
})
export class DatetimePickerComponent<D>
Expand Down Expand Up @@ -332,6 +341,9 @@ export class DatetimePickerComponent<D>
@Input()
mobilePortrait = false;

/** To set input width 100% */
@Input() isFullWidth = false;

/** Event emitted when the state of the isOpen property changes. */
@Output()
isOpenChange = new EventEmitter<boolean>();
Expand Down Expand Up @@ -751,6 +763,9 @@ export class DatetimePickerComponent<D>
this.onChange(null);
return;
}

inputStr = convertToDesiredFormat(inputStr.toString());

this.date = this._parseDate(inputStr);
this._isInvalidDateInput = !this._isModelValid(this.date);

Expand Down
66 changes: 65 additions & 1 deletion libs/core/datetime/datetime-format.pipes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Inject, Optional, Pipe, PipeTransform } from '@angular/core';
import { Inject, Optional, Pipe, PipeTransform, Signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FD_LANGUAGE, FdLanguage, FdLanguageKeyIdentifier, TranslationResolver } from '@fundamental-ngx/i18n';
import { Observable } from 'rxjs';
import { DatetimeAdapter } from './datetime-adapter';
import { DATE_TIME_FORMATS, DateTimeFormats } from './datetime-formats';

Expand Down Expand Up @@ -66,3 +69,64 @@ export class DateFromNowPipe<D> implements PipeTransform {
}
}
}

@Pipe({
name: 'dayPeriodFormat',
standalone: true
})
export class DayPeriodFormatPipe<D> implements PipeTransform {
/** @hidden */
constructor(
private _dateTimeAdapter: DatetimeAdapter<D>,
@Optional() @Inject(DATE_TIME_FORMATS) private _dateTimeFormats: DateTimeFormats
) {}

/** Format date object for day period */
transform(date: D, customFormat: any = {}, noDateMessage = ''): string {
if (!date) {
return noDateMessage;
}

return this._dateTimeAdapter.format(date, customFormat);
}
}

@Pipe({
name: 'translateDayPeriod',
standalone: true,
pure: false // required to update the value when the observable is resolved
})
export class TranslateDayPeriodPipe implements PipeTransform {
/** @hidden */
private readonly _translationResolver = new TranslationResolver();

/** Signal that will hold the current language */
private readonly _currentLanguageSignal: Signal<FdLanguage>;

/** @hidden */
constructor(@Inject(FD_LANGUAGE) private _language$: Observable<FdLanguage>) {
this._currentLanguageSignal = toSignal(this._language$, { initialValue: {} as FdLanguage });
}

/** Format date object for day period */
transform(value: string | null): string | null {
if (!value) {
return value;
}

const dayPeriodPattern = /(coreTime\.\w+Label)/;
const match = value.match(dayPeriodPattern);

if (match?.[0]) {
const currentLanguage = this._currentLanguageSignal();
const translatedValue = this._translationResolver.resolve(
currentLanguage,
match[0] as FdLanguageKeyIdentifier
);
return translatedValue ? value.replace(dayPeriodPattern, translatedValue) : value;
}

// If no day period pattern is found, return the original value
return value;
}
}
68 changes: 68 additions & 0 deletions libs/core/datetime/datetime-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Converts a given time string to a desired format.
*
* @param {string} inputStr - The input time string to be converted.
* @returns {string} - The converted time string in the desired format.
*/
export function convertToDesiredFormat(inputStr: string): string {
const dayPeriodMapping = {
morning: 0,
afternoon: 12,
evening: 12,
night: 0
};

const dayPeriodPattern = /\b(morning|afternoon|evening|night)\b/;
const amPmPattern = /\b(AM|PM)\b/i;

const convertDayPeriod = (input: string): string => {
const match = input.match(dayPeriodPattern);
if (match) {
const period = match[0].toLowerCase();
const timeShift = dayPeriodMapping[period];
input = input.replace(dayPeriodPattern, '').trim();
const timeMatch = input.match(/\d{1,2}:\d{2}/);
if (timeMatch) {
const [hoursStr, minutesStr] = timeMatch[0].split(':');
let hours = Number(hoursStr);
const minutes = Number(minutesStr);
hours = (hours % 12) + timeShift;
input = input.replace(
timeMatch[0],
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
);
}
}
return input;
};

const convertAmPm = (input: string): string => {
const match = input.match(amPmPattern);
if (match) {
const period = match[0].toUpperCase();
input = input.replace(amPmPattern, '').trim();
const timeMatch = input.match(/\d{1,2}:\d{2}/);
if (timeMatch) {
const [hoursStr, minutesStr] = timeMatch[0].split(':');
let hours = Number(hoursStr);
const minutes = Number(minutesStr);
if (period === 'PM' && hours < 12) {
hours += 12;
}
if (period === 'AM' && hours === 12) {
hours = 0;
}
input = input.replace(
timeMatch[0],
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
);
}
}
return input;
};

inputStr = convertDayPeriod(inputStr);
inputStr = convertAmPm(inputStr);

return inputStr.replace(/\bat\b/i, '').trim();
}
63 changes: 54 additions & 9 deletions libs/core/datetime/fd-datetime-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ import { Inject, Injectable, LOCALE_ID, Optional } from '@angular/core';

import { INVALID_DATE_ERROR, LETTERS_UNICODE_RANGE } from '@fundamental-ngx/cdk/utils';

import { FdLanguageKeyIdentifier } from '@fundamental-ngx/i18n';
import { DatetimeAdapter } from './datetime-adapter';
import { FdDate } from './fd-date';
import { range, toIso8601 } from './fd-date.utils';

const AM_DAY_PERIOD_DEFAULT = 'AM';
const PM_DAY_PERIOD_DEFAULT = 'PM';

type CustomDateTimeFormatOptions = Omit<Intl.DateTimeFormatOptions, 'dayPeriod'> & {
dayPeriod?: boolean;
};

/**
* DatetimeAdapter implementation based on FdDate.
*
* This uses FdDate as a date model and relies on Intl.DateTimeFormat
* for formatting and translation purposes.
*
*/

@Injectable()
export class FdDatetimeAdapter extends DatetimeAdapter<FdDate> {
/** Whether to clamp the date between 1 and 9999 to avoid IE and Edge errors. */
Expand All @@ -26,9 +30,7 @@ export class FdDatetimeAdapter extends DatetimeAdapter<FdDate> {
/** @hidden */
constructor(@Optional() @Inject(LOCALE_ID) localeId: string, platform: Platform) {
super();

super.setLocale(localeId);

this._fixYearsRangeIssue = platform.TRIDENT || platform.EDGE;
}

Expand Down Expand Up @@ -253,7 +255,7 @@ export class FdDatetimeAdapter extends DatetimeAdapter<FdDate> {
}

/** Format date object to string */
format(date: FdDate, displayFormat: Intl.DateTimeFormatOptions): string {
format(date: FdDate, displayFormat: CustomDateTimeFormatOptions): string {
if (!this.isValid(date)) {
return INVALID_DATE_ERROR;
}
Expand All @@ -264,12 +266,11 @@ export class FdDatetimeAdapter extends DatetimeAdapter<FdDate> {
date = this.clone(date);
date.year = Math.max(1, Math.min(9999, date.year));
}

displayFormat = { ...displayFormat, timeZone: 'utc' };

const dateTimeFormatter = new Intl.DateTimeFormat(this.locale, displayFormat);
const dateInstance = this._createDateInstanceByFdDate(date);
return this._stripDirectionalityCharacters(this._format(dateTimeFormatter, dateInstance));
return displayFormat.dayPeriod
? this._formatWithDayPeriod(date, displayFormat)
: this._formatWithIntl(date, displayFormat);
}

/** Add years to a date */
Expand Down Expand Up @@ -345,7 +346,7 @@ export class FdDatetimeAdapter extends DatetimeAdapter<FdDate> {
}

/** Check if a time format includes a day period */
isTimeFormatIncludesDayPeriod(displayFormat: Intl.DateTimeFormatOptions): boolean {
isTimeFormatIncludesDayPeriod(displayFormat: CustomDateTimeFormatOptions): boolean {
if (typeof displayFormat?.hour12 === 'boolean') {
return displayFormat.hour12;
}
Expand All @@ -369,6 +370,50 @@ export class FdDatetimeAdapter extends DatetimeAdapter<FdDate> {
return typeof displayFormat?.second === 'string';
}

/** Format with custom pattern including day period names */
private _formatWithDayPeriod(date: FdDate, displayFormat: CustomDateTimeFormatOptions): string {
const formattedTime = this._formatWithIntl(date, { ...displayFormat, dayPeriod: undefined });
const dayPeriodName = this._getDayPeriodName(date);

return this._insertDayPeriod(formattedTime, dayPeriodName, displayFormat);
}

/** Insert day period into the formatted string */
private _insertDayPeriod(
formattedTime: string,
dayPeriodName: FdLanguageKeyIdentifier,
displayFormat: CustomDateTimeFormatOptions
): string {
if (displayFormat.year || displayFormat.month || displayFormat.day) {
return formattedTime
.replace(/(\d{1,2}:\d{2})( [AP]M)?/, `$1 ${dayPeriodName}`)
.replace(/,\s*(\d{1,2}:\d{2})/, `, at $1 ${dayPeriodName}`);
} else {
return formattedTime.replace(/(\d{1,2}:\d{2}(:\d{2})?)/, `$1 ${dayPeriodName}`);
}
}

/** Get day period name based on the hour */
private _getDayPeriodName(date: FdDate): FdLanguageKeyIdentifier {
const hour = date.hour;
if (hour < 6) {
return 'coreTime.nightLabel';
} else if (hour < 12) {
return 'coreTime.morningLabel';
} else if (hour < 18) {
return 'coreTime.afternoonLabel';
} else {
return 'coreTime.eveningLabel';
}
}

/** Format date using Intl.DateTimeFormat */
private _formatWithIntl(date: FdDate, displayFormat: any): string {
const formatter = new Intl.DateTimeFormat(this.locale, displayFormat);
const dateInstance = this._createDateInstanceByFdDate(date);
return this._stripDirectionalityCharacters(this._format(formatter, dateInstance));
}

/**
* @hidden
* Strip out unicode LTR and RTL characters. Edge and IE insert these into formatted dates while
Expand Down
16 changes: 14 additions & 2 deletions libs/core/datetime/fd-datetime-pipes.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { NgModule, Type } from '@angular/core';
import { DateFormatPipe, DateFromNowPipe, DateTimeFormatPipe } from './datetime-format.pipes';
import {
DateFormatPipe,
DateFromNowPipe,
DateTimeFormatPipe,
DayPeriodFormatPipe,
TranslateDayPeriodPipe
} from './datetime-format.pipes';

const PIPES: Type<unknown>[] = [DateFormatPipe, DateTimeFormatPipe, DateFromNowPipe];
const PIPES: Type<unknown>[] = [
DateFormatPipe,
DateTimeFormatPipe,
DateFromNowPipe,
DayPeriodFormatPipe,
TranslateDayPeriodPipe
];

/**
* @deprecated
Expand Down
1 change: 1 addition & 0 deletions libs/core/datetime/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './datetime-adapter';
export * from './datetime-format.pipes';
export * from './datetime-formats';
export * from './datetime-utils';
export * from './fd-date';
export * from './fd-date-formats';
export * from './fd-date.utils';
Expand Down
2 changes: 1 addition & 1 deletion libs/core/time-picker/time-picker.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
type="text"
class="fd-input"
fd-input-group-input
[value]="_inputTimeValue"
[value]="_inputTimeValue | translateDayPeriod"
(focusout)="_timeInputChanged($any($event.currentTarget).value)"
(keyup.enter)="_timeInputChanged($any($event.currentTarget).value)"
[disabled]="disabled"
Expand Down
Loading

0 comments on commit c70f83a

Please sign in to comment.