Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 81 additions & 6 deletions src/components/calendar/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,31 @@ const WEEK_DAYS_MAP = {
thursday: 4,
friday: 5,
saturday: 6,
};
} as const;

/* Converter functions */

export function isValidDate(date: Date): Date | null {
return Number.isNaN(date.valueOf()) ? null : date;
/**
* Type guard to check if a value is a valid Date object.
*/
export function isValidDate(value: unknown): value is Date {
if (value instanceof Date) {
return !Number.isNaN(value.getTime());
}
return false;
}

export function parseISODate(string: string): Date | null {
// ISO date format (YYYY-MM-DD)
if (ISO_DATE_PATTERN.test(string)) {
const timeComponent = !string.includes('T') ? 'T00:00:00' : '';
return isValidDate(new Date(`${string}${timeComponent}`));
return getValidDate(new Date(`${string}${timeComponent}`));
}

// Time format (HH:MM:SS)
if (TIME_PATTERN.test(string)) {
const today = first(new Date().toISOString().split('T'));
return isValidDate(new Date(`${today}T${string}`));
return getValidDate(new Date(`${today}T${string}`));
}

return null;
Expand All @@ -74,7 +80,7 @@ export function convertToDate(value?: Date | string | null): Date | null {
return null;
}

return isString(value) ? parseISODate(value) : isValidDate(value);
return isString(value) ? parseISODate(value) : getValidDate(value);
}

/**
Expand Down Expand Up @@ -311,3 +317,72 @@ export function createDateConstraints(

return constraints.length > 0 ? constraints : undefined;
}

function getValidDate(date: Date): Date | null {
return Number.isNaN(date.valueOf()) ? null : date;
}

/**
* Checks if a date is greater than a maximum date value.
*/
export function isDateExceedingMax(
value: Date,
maxValue: Date,
includeTime = true,
includeDate = true
): boolean {
return compareDates(
value,
maxValue,
(a, b) => a > b,
includeTime,
includeDate
);
}

/**
* Checks if a date is less than a minimum date value.
*/
export function isDateLessThanMin(
value: Date,
minValue: Date,
includeTime = true,
includeDate = true
): boolean {
return compareDates(
value,
minValue,
(a, b) => a < b,
includeTime,
includeDate
);
}

/**
* Compares two dates with optional time/date exclusions.
*/
function compareDates(
value: Date,
boundary: Date,
comparator: (a: number, b: number) => boolean,
includeTime: boolean,
includeDate: boolean
): boolean {
if (includeTime && includeDate) {
return comparator(value.getTime(), boundary.getTime());
}

const v = new Date(value.getTime());
const b = new Date(boundary.getTime());

if (!includeTime) {
v.setHours(0, 0, 0, 0);
b.setHours(0, 0, 0, 0);
}
if (!includeDate) {
v.setFullYear(0, 0, 0);
b.setFullYear(0, 0, 0);
}

return comparator(v.getTime(), b.getTime());
}
68 changes: 67 additions & 1 deletion src/components/common/i18n/i18n-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
getCurrentI18n,
getDateFormatter,
getDisplayNamesFormatter,
getI18nManager,
type IResourceChangeEventArgs,
Expand Down Expand Up @@ -119,7 +120,7 @@ class I18nController<T extends object> implements ReactiveController {
/** @internal */
public handleEvent(event: CustomEvent<IResourceChangeEventArgs>): void {
this._defaultResourceStrings = this._getCurrentResourceStrings();
this._resourceChangeCallback?.(event);
this._resourceChangeCallback?.call(this._host, event);
this._host.requestUpdate();
}

Expand Down Expand Up @@ -203,6 +204,71 @@ class I18nController<T extends object> implements ReactiveController {
//#endregion
}

/**
* Formats a date for display based on the specified format and locale.
*/
type DateTimeStyle = 'short' | 'long' | 'medium' | 'full';

const DATE_TIME_STYLES = new Set<string>(['short', 'long', 'medium', 'full']);

function extractStyle(format: string, suffix: string): DateTimeStyle {
return format.toLowerCase().split(suffix)[0] as DateTimeStyle;
}

/** Returns the default date-time input format for a given locale */
export function getDefaultDateTimeFormat(locale: string): string {
return getDateFormatter().getLocaleDateTimeFormat(locale, true);
}

/** Returns the date-time format string with the appropriate suffix if it's a predefined style */
export function getDateTimeFormat(
format?: string,
suffix: 'Date' | 'Time' = 'Date'
): string | undefined {
return format && DATE_TIME_STYLES.has(format) ? `${format}${suffix}` : format;
}

/**
* Formats a date for display using the specified format.
*/
export function formatDisplayDate(
value: Date,
locale: string,
displayFormat?: string
): string {
if (!displayFormat) {
return getDateFormatter().formatDateTime(value, locale, {});
}

// Full date+time styles (short, long, medium, full)
if (DATE_TIME_STYLES.has(displayFormat)) {
const style = displayFormat as DateTimeStyle;
return getDateFormatter().formatDateTime(value, locale, {
dateStyle: style,
timeStyle: style,
});
}

// Date-only styles (shortDate, longDate, etc.)
if (displayFormat.endsWith('Date')) {
return getDateFormatter().formatDateTime(value, locale, {
dateStyle: extractStyle(displayFormat, 'date'),
});
}

// Time-only styles (shortTime, longTime, etc.)
if (displayFormat.endsWith('Time')) {
return getDateFormatter().formatDateTime(value, locale, {
timeStyle: extractStyle(displayFormat, 'time'),
});
}

// Custom format string
return getDateFormatter().formatDateCustomFormat(value, displayFormat, {
locale,
});
}

/** Factory function to create and attach the I18nController to a host. */
export function addI18nController<T extends object>(
host: I18nControllerHost,
Expand Down
11 changes: 6 additions & 5 deletions src/components/date-picker/date-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import {
IgcCalendarResourceStringEN,
type IgcCalendarResourceStrings,
} from '../common/i18n/EN/calendar.resources.js';
import { addI18nController } from '../common/i18n/i18n-controller.js';
import {
addI18nController,
getDateTimeFormat,
} from '../common/i18n/i18n-controller.js';
import { IgcBaseComboBoxLikeComponent } from '../common/mixins/combo-box.js';
import type { AbstractConstructor } from '../common/mixins/constructor.js';
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
Expand All @@ -39,8 +42,8 @@ import {
equal,
findElementFromEventPath,
} from '../common/util.js';
import type { DatePart } from '../date-time-input/date-part.js';
import IgcDateTimeInputComponent from '../date-time-input/date-time-input.js';
import { type DatePart, DateTimeUtil } from '../date-time-input/date-util.js';
import IgcDialogComponent from '../dialog/dialog.js';
import IgcFocusTrapComponent from '../focus-trap/focus-trap.js';
import IgcIconComponent from '../icon/icon.js';
Expand Down Expand Up @@ -828,9 +831,7 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin(
}

protected _renderInput(id: string) {
const format = DateTimeUtil.predefinedToDateDisplayFormat(
this._displayFormat
);
const format = getDateTimeFormat(this._displayFormat);

// Dialog mode is always readonly, rest depends on configuration
const readOnly = !this._isDropDown || this.readOnly || this.nonEditable;
Expand Down
21 changes: 10 additions & 11 deletions src/components/date-range-picker/date-range-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ import {
type IgcDateRangePickerResourceStrings,
IgcDateRangePickerResourceStringsEN,
} from '../common/i18n/EN/date-range-picker.resources.js';
import { addI18nController } from '../common/i18n/i18n-controller.js';
import {
addI18nController,
formatDisplayDate,
getDateTimeFormat,
getDefaultDateTimeFormat,
} from '../common/i18n/i18n-controller.js';
import { IgcBaseComboBoxLikeComponent } from '../common/mixins/combo-box.js';
import type { AbstractConstructor } from '../common/mixins/constructor.js';
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
Expand All @@ -52,7 +57,6 @@ import {
isEmpty,
} from '../common/util.js';
import IgcDateTimeInputComponent from '../date-time-input/date-time-input.js';
import { DateTimeUtil } from '../date-time-input/date-util.js';
import IgcDialogComponent from '../dialog/dialog.js';
import IgcFocusTrapComponent from '../focus-trap/focus-trap.js';
import IgcIconComponent from '../icon/icon.js';
Expand Down Expand Up @@ -238,9 +242,7 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM
protected readonly _i18nController =
addI18nController<IgcDateRangePickerResourceStrings>(this, {
defaultEN: IgcDateRangePickerResourceStringsEN,
onResourceChange: () => {
this._updateDefaultMask();
},
onResourceChange: this._updateDefaultMask,
});

private _activeDate: Date | null = null;
Expand Down Expand Up @@ -675,7 +677,7 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM

@watch('locale')
protected _updateDefaultMask(): void {
this._defaultMask = DateTimeUtil.getDefaultInputMask(this.locale);
this._defaultMask = getDefaultDateTimeFormat(this.locale);
this._defaultDisplayFormat = getDateFormatter().getLocaleDateTimeFormat(
this.locale
);
Expand Down Expand Up @@ -891,9 +893,8 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM
return;
}

const { formatDisplayDate, predefinedToDateDisplayFormat } = DateTimeUtil;
const { start, end } = this.value;
const displayFormat = predefinedToDateDisplayFormat(this.displayFormat);
const displayFormat = getDateTimeFormat(this.displayFormat);

const startValue = formatDisplayDate(start, this.locale, displayFormat);
const endValue = formatDisplayDate(end, this.locale, displayFormat);
Expand Down Expand Up @@ -1120,9 +1121,7 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM
const placeholder =
picker === 'start' ? this.placeholderStart : this.placeholderEnd;
const label = picker === 'start' ? this.labelStart : this.labelEnd;
const format = DateTimeUtil.predefinedToDateDisplayFormat(
this._displayFormat
);
const format = getDateTimeFormat(this._displayFormat);
const value = picker === 'start' ? this.value?.start : this.value?.end;

const prefixes =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { elementUpdated, expect } from '@open-wc/testing';
import IgcCalendarComponent from '../calendar/calendar.js';
import { getCalendarDOM, getDOMDate } from '../calendar/helpers.spec.js';
import type { CalendarDay } from '../calendar/model.js';
import { formatDisplayDate } from '../common/i18n/i18n-controller.js';
import { equal } from '../common/util.js';
import { checkDatesEqual, simulateClick } from '../common/utils.spec.js';
import IgcDateTimeInputComponent from '../date-time-input/date-time-input.js';
import { DateTimeUtil } from '../date-time-input/date-util.js';
import IgcInputComponent from '../input/input.js';
import type IgcDateRangePickerComponent from './date-range-picker.js';
import type { DateRangeValue } from './date-range-picker.js';
Expand Down Expand Up @@ -52,14 +52,14 @@ export const checkSelectedRange = (
} else {
const input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!;
const start = expectedValue?.start
? DateTimeUtil.formatDisplayDate(
? formatDisplayDate(
expectedValue.start,
picker.locale,
picker.displayFormat
)
: '';
const end = expectedValue?.end
? DateTimeUtil.formatDisplayDate(
? formatDisplayDate(
expectedValue.end,
picker.locale,
picker.displayFormat
Expand Down
Loading