diff --git a/core/api.txt b/core/api.txt
index fa3825d290..e256b31222 100644
--- a/core/api.txt
+++ b/core/api.txt
@@ -394,6 +394,7 @@ ion-datetime,prop,dayValues,number | number[] | string | undefined,undefined,fal
ion-datetime,prop,disabled,boolean,false,false,false
ion-datetime,prop,doneText,string,'Done',false,false
ion-datetime,prop,firstDayOfWeek,number,0,false,false
+ion-datetime,prop,formatOptions,undefined | { date?: DateTimeFormatOptions | undefined; time?: DateTimeFormatOptions | undefined; },undefined,false,false
ion-datetime,prop,highlightedDates,((dateIsoString: string) => DatetimeHighlightStyle | undefined) | DatetimeHighlight[] | undefined,undefined,false,false
ion-datetime,prop,hourCycle,"h11" | "h12" | "h23" | "h24" | undefined,undefined,false,false
ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false
diff --git a/core/src/components.d.ts b/core/src/components.d.ts
index bcea90eeb2..efbc4bcebe 100644
--- a/core/src/components.d.ts
+++ b/core/src/components.d.ts
@@ -858,6 +858,10 @@ export namespace Components {
* The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday.
*/
"firstDayOfWeek": number;
+ /**
+ * Formatting options, separated by date and time.
+ */
+ "formatOptions"?: { date?: Intl.DateTimeFormatOptions; time?: Intl.DateTimeFormatOptions };
/**
* Used to apply custom text and background colors to specific dates. Can be either an array of objects containing ISO strings and colors, or a callback that receives an ISO string and returns the colors. Only applies to the `date`, `date-time`, and `time-date` presentations, with `preferWheel="false"`.
*/
@@ -5541,6 +5545,10 @@ declare namespace LocalJSX {
* The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday.
*/
"firstDayOfWeek"?: number;
+ /**
+ * Formatting options, separated by date and time.
+ */
+ "formatOptions"?: { date?: Intl.DateTimeFormatOptions; time?: Intl.DateTimeFormatOptions };
/**
* Used to apply custom text and background colors to specific dates. Can be either an array of objects containing ISO strings and colors, or a callback that receives an ISO string and returns the colors. Only applies to the `date`, `date-time`, and `time-date` presentations, with `preferWheel="false"`.
*/
diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx
index 57f275e25f..b9c1afe6fa 100644
--- a/core/src/components/datetime/datetime.tsx
+++ b/core/src/components/datetime/datetime.tsx
@@ -171,6 +171,19 @@ export class Datetime implements ComponentInterface {
*/
@Prop() disabled = false;
+ /**
+ * Formatting options, separated by date and time.
+ */
+ @Prop() formatOptions?: { date?: Intl.DateTimeFormatOptions; time?: Intl.DateTimeFormatOptions };
+
+ @Watch('formatOptions')
+ protected formatOptionsChanged(formatOptions: {
+ date?: Intl.DateTimeFormatOptions;
+ time?: Intl.DateTimeFormatOptions;
+ }) {
+ this.errorIfTimeZoneProvided(formatOptions);
+ }
+
/**
* If `true`, the datetime appears normal but the selected date cannot be changed.
*/
@@ -1357,7 +1370,7 @@ export class Datetime implements ComponentInterface {
};
componentWillLoad() {
- const { el, highlightedDates, multiple, presentation, preferWheel } = this;
+ const { el, formatOptions, highlightedDates, multiple, presentation, preferWheel } = this;
if (multiple) {
if (presentation !== 'date') {
@@ -1382,6 +1395,10 @@ export class Datetime implements ComponentInterface {
}
}
+ if (formatOptions) {
+ this.errorIfTimeZoneProvided(formatOptions);
+ }
+
const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues));
const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues));
@@ -1409,6 +1426,20 @@ export class Datetime implements ComponentInterface {
this.emitStyle();
}
+ private errorIfTimeZoneProvided(formatOptions: {
+ date?: Intl.DateTimeFormatOptions;
+ time?: Intl.DateTimeFormatOptions;
+ }) {
+ if (
+ formatOptions?.date?.timeZone ||
+ formatOptions?.time?.timeZone ||
+ formatOptions?.date?.timeZoneName ||
+ formatOptions?.time?.timeZoneName
+ ) {
+ printIonWarning('Datetime: "timeZone" and "timeZoneName" are not supported in "formatOptions".');
+ }
+ }
+
private emitStyle() {
this.ionStyle.emit({
interactive: true,
@@ -2354,10 +2385,16 @@ export class Datetime implements ComponentInterface {
}
private renderTimeOverlay() {
- const { disabled, hourCycle, isTimePopoverOpen, locale } = this;
+ const { disabled, formatOptions, hourCycle, isTimePopoverOpen, locale } = this;
const computedHourCycle = getHourCycle(locale, hourCycle);
const activePart = this.getActivePartsWithFallback();
+ const timeButtonFormatOptions = formatOptions?.time || {
+ hour: 'numeric',
+ minute: 'numeric',
+ computedHourCycle,
+ };
+
return [
,
,
{
});
});
});
+
+/**
+ * This behavior does not differ across
+ * directions.
+ */
+configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
+ test.describe(title('datetime: formatOptions'), () => {
+ test('should format header and time button', async ({ page }) => {
+ await page.setContent(
+ `
+
+ Select Date
+
+ `,
+ config
+ );
+
+ await page.locator('.datetime-ready').waitFor();
+
+ const datetime = page.locator('ion-datetime');
+
+ await datetime.evaluate(
+ (el: HTMLIonDatetimeElement) =>
+ (el.formatOptions = {
+ time: { hour: '2-digit', minute: '2-digit' },
+ date: { day: '2-digit', month: 'long', era: 'short' },
+ })
+ );
+
+ await page.waitForChanges();
+
+ const headerDate = page.locator('ion-datetime .datetime-selected-date');
+ await expect(headerDate).toHaveText('February 01 AD');
+
+ const timeBody = page.locator('ion-datetime .time-body');
+ await expect(timeBody).toHaveText('04:30 PM');
+
+ await expect(datetime).toHaveScreenshot(screenshot('datetime-format-options'));
+ });
+ });
+});
+
+/**
+ * This behavior does not differ across
+ * modes/directions.
+ */
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('datetime: formatOptions timeZone error'), () => {
+ test('should throw a warning if time zone is provided', async ({ page }) => {
+ const logs: string[] = [];
+
+ page.on('console', (msg) => {
+ if (msg.type() === 'warning') {
+ logs.push(msg.text());
+ }
+ });
+
+ await page.setContent(
+ `
+
+ Select Date
+
+ `,
+ config
+ );
+
+ const datetime = page.locator('ion-datetime');
+
+ await datetime.evaluate(
+ (el: HTMLIonDatetimeElement) =>
+ (el.formatOptions = {
+ time: { timeZone: 'UTC' },
+ })
+ );
+
+ await page.locator('.datetime-ready').waitFor();
+
+ await page.waitForChanges();
+
+ expect(logs.length).toBe(1);
+ expect(logs[0]).toContain(
+ '[Ionic Warning]: Datetime: "timeZone" and "timeZoneName" are not supported in "formatOptions".'
+ );
+ });
+ });
+});
diff --git a/core/src/components/datetime/test/format.spec.ts b/core/src/components/datetime/test/format.spec.ts
index 5ff218167d..7b595f687c 100644
--- a/core/src/components/datetime/test/format.spec.ts
+++ b/core/src/components/datetime/test/format.spec.ts
@@ -53,6 +53,46 @@ describe('getMonthAndDay()', () => {
it('should return sáb, 1 abr', () => {
expect(getMonthAndDay('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('sáb, 1 abr');
});
+
+ it('should use formatOptions', () => {
+ const datetimeParts: DatetimeParts = {
+ day: 1,
+ month: 1,
+ year: 2022,
+ hour: 9,
+ minute: 40,
+ };
+
+ const formatOptions: Intl.DateTimeFormatOptions = {
+ day: '2-digit',
+ weekday: 'long',
+ month: 'narrow',
+ hour: '2-digit',
+ minute: '2-digit',
+ };
+
+ // Even though this method is intended to be used for date, the time may be displayed as well when passing formatOptions
+ expect(getMonthAndDay('en-US', datetimeParts, formatOptions)).toEqual('Saturday, J 01, 09:40 AM');
+ });
+
+ it('should override provided time zone with UTC', () => {
+ const datetimeParts: DatetimeParts = {
+ day: 1,
+ month: 1,
+ year: 2022,
+ hour: 23,
+ minute: 40,
+ };
+
+ const formatOptions: Intl.DateTimeFormatOptions = {
+ timeZone: 'Australia/Sydney',
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ };
+
+ expect(getMonthAndDay('en-US', datetimeParts, formatOptions)).toEqual('Sat, Jan 1');
+ });
});
describe('getFormattedHour()', () => {
@@ -144,6 +184,7 @@ describe('getLocalizedTime', () => {
expect(getLocalizedTime('en-GB', datetimeParts, 'h12')).toEqual('12:00 am');
});
+
it('should parse time-only values correctly', () => {
const datetimeParts: Partial = {
hour: 22,
@@ -153,4 +194,42 @@ describe('getLocalizedTime', () => {
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h23')).toEqual('22:40');
});
+
+ it('should use formatOptions', () => {
+ const datetimeParts: DatetimeParts = {
+ day: 1,
+ month: 1,
+ year: 2022,
+ hour: 9,
+ minute: 40,
+ };
+
+ const formatOptions: Intl.DateTimeFormatOptions = {
+ hour: '2-digit',
+ minute: '2-digit',
+ dayPeriod: 'short',
+ day: '2-digit',
+ };
+
+ // Even though this method is intended to be used for time, the date may be displayed as well when passing formatOptions
+ expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('01, 09:40 in the morning');
+ });
+
+ it('should override provided time zone with UTC', () => {
+ const datetimeParts: DatetimeParts = {
+ day: 1,
+ month: 1,
+ year: 2022,
+ hour: 9,
+ minute: 40,
+ };
+
+ const formatOptions: Intl.DateTimeFormatOptions = {
+ timeZone: 'Australia/Sydney',
+ hour: 'numeric',
+ minute: 'numeric',
+ };
+
+ expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('9:40 AM');
+ });
});
diff --git a/core/src/components/datetime/utils/format.ts b/core/src/components/datetime/utils/format.ts
index 0f70299dc5..6589c10e40 100644
--- a/core/src/components/datetime/utils/format.ts
+++ b/core/src/components/datetime/utils/format.ts
@@ -11,7 +11,12 @@ const getFormattedDayPeriod = (dayPeriod?: string) => {
return dayPeriod.toUpperCase();
};
-export const getLocalizedTime = (locale: string, refParts: DatetimeParts, hourCycle: DatetimeHourCycle): string => {
+export const getLocalizedTime = (
+ locale: string,
+ refParts: DatetimeParts,
+ hourCycle: DatetimeHourCycle,
+ formatOptions?: Intl.DateTimeFormatOptions
+): string => {
const timeParts: Pick = {
hour: refParts.hour,
minute: refParts.minute,
@@ -21,9 +26,18 @@ export const getLocalizedTime = (locale: string, refParts: DatetimeParts, hourCy
return 'Invalid Time';
}
+ const defaultFormatOptions: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' };
+
+ // If any options are provided, don't use any of the defaults.
+ const options: Intl.DateTimeFormatOptions = formatOptions ?? defaultFormatOptions;
+
return new Intl.DateTimeFormat(locale, {
- hour: 'numeric',
- minute: 'numeric',
+ ...options,
+ /**
+ * We use hourCycle here instead of hour12 due to:
+ * https://bugs.chromium.org/p/chromium/issues/detail?id=1347316&q=hour12&can=2
+ */
+ hourCycle,
/**
* Setting the timeZone to UTC prevents
* new Intl.DatetimeFormat from subtracting
@@ -31,11 +45,6 @@ export const getLocalizedTime = (locale: string, refParts: DatetimeParts, hourCy
* when formatting the time.
*/
timeZone: 'UTC',
- /**
- * We use hourCycle here instead of hour12 due to:
- * https://bugs.chromium.org/p/chromium/issues/detail?id=1347316&q=hour12&can=2
- */
- hourCycle,
/**
* Setting Z at the end indicates that this
* date string is in the UTC time zone. This
@@ -150,11 +159,17 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D
* Gets the day of the week, month, and day
* Used for the header in MD mode.
*/
-export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => {
+export const getMonthAndDay = (locale: string, refParts: DatetimeParts, formatOptions?: Intl.DateTimeFormatOptions) => {
+ const defaultFormatOptions: Intl.DateTimeFormatOptions = { weekday: 'short', month: 'short', day: 'numeric' };
+
+ // If any options are provided, don't use any of the defaults. This way the developer can (for example) choose to not have the weekday displayed at all.
+ const options: Intl.DateTimeFormatOptions = formatOptions ?? defaultFormatOptions;
+
const date = getNormalizedDate(refParts);
- return new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }).format(
- date
- );
+ return new Intl.DateTimeFormat(locale, {
+ ...options,
+ timeZone: 'UTC',
+ }).format(date);
};
/**
diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts
index be168ed870..a592c55894 100644
--- a/packages/angular/src/directives/proxies.ts
+++ b/packages/angular/src/directives/proxies.ts
@@ -635,7 +635,7 @@ Set `scrollEvents` to `true` to enable.
@ProxyCmp({
- inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'],
+ inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'formatOptions', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'],
methods: ['confirm', 'reset', 'cancel']
})
@Component({
@@ -643,7 +643,7 @@ Set `scrollEvents` to `true` to enable.
changeDetection: ChangeDetectionStrategy.OnPush,
template: '',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
- inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'],
+ inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'formatOptions', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'],
})
export class IonDatetime {
protected el: HTMLElement;
diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts
index 1ea101fb23..57380e47bd 100644
--- a/packages/vue/src/proxies.ts
+++ b/packages/vue/src/proxies.ts
@@ -274,6 +274,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer