diff --git a/core/api.txt b/core/api.txt index fa3825d2903..acdcb8d0faf 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; time?: DateTimeFormatOptions | undefined; } | { date?: DateTimeFormatOptions | undefined; time: DateTimeFormatOptions; },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 a706eb8971d..ab938b5bc16 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -15,7 +15,7 @@ import { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface"; import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface"; import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface"; -import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; +import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; import { SpinnerTypes } from "./components/spinner/spinner-configs"; import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; import { CounterFormatter } from "./components/item/item-interface"; @@ -51,7 +51,7 @@ export { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface"; export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface"; export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface"; -export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; +export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; export { SpinnerTypes } from "./components/spinner/spinner-configs"; export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; export { CounterFormatter } from "./components/item/item-interface"; @@ -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 for dates and times. Should include a 'date' and/or 'time' object, each of which is of type [Intl.DateTimeFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options). + */ + "formatOptions"?: FormatOptions; /** * 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 for dates and times. Should include a 'date' and/or 'time' object, each of which is of type [Intl.DateTimeFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options). + */ + "formatOptions"?: FormatOptions; /** * 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-button/datetime-button.tsx b/core/src/components/datetime-button/datetime-button.tsx index 7ce3ca4163d..519fb42da6a 100644 --- a/core/src/components/datetime-button/datetime-button.tsx +++ b/core/src/components/datetime-button/datetime-button.tsx @@ -8,7 +8,7 @@ import { getIonMode } from '../../global/ionic-global'; import type { Color } from '../../interface'; import type { DatetimePresentation } from '../datetime/datetime-interface'; import { getToday } from '../datetime/utils/data'; -import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format'; +import { getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format'; import { getHourCycle } from '../datetime/utils/helpers'; import { parseDate } from '../datetime/utils/parse'; /** @@ -196,7 +196,7 @@ export class DatetimeButton implements ComponentInterface { return; } - const { value, locale, hourCycle, preferWheel, multiple, titleSelectedDatesFormatter } = datetimeEl; + const { value, locale, formatOptions, hourCycle, preferWheel, multiple, titleSelectedDatesFormatter } = datetimeEl; const parsedValues = this.getParsedDateValues(value); @@ -225,8 +225,12 @@ export class DatetimeButton implements ComponentInterface { switch (datetimePresentation) { case 'date-time': case 'time-date': - const dateText = getMonthDayAndYear(locale, firstParsedDatetime); - const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle); + const dateText = getLocalizedDateTime( + locale, + firstParsedDatetime, + formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' } + ); + const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle, formatOptions?.time); if (preferWheel) { this.dateText = `${dateText} ${timeText}`; } else { @@ -246,20 +250,28 @@ export class DatetimeButton implements ComponentInterface { } this.dateText = headerText; } else { - this.dateText = getMonthDayAndYear(locale, firstParsedDatetime); + this.dateText = getLocalizedDateTime( + locale, + firstParsedDatetime, + formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' } + ); } break; case 'time': - this.timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle); + this.timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle, formatOptions?.time); break; case 'month-year': - this.dateText = getMonthAndYear(locale, firstParsedDatetime); + this.dateText = getLocalizedDateTime( + locale, + firstParsedDatetime, + formatOptions?.date ?? { month: 'long', year: 'numeric' } + ); break; case 'month': - this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, { month: 'long' }); + this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, formatOptions?.time ?? { month: 'long' }); break; case 'year': - this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, { year: 'numeric' }); + this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, formatOptions?.time ?? { year: 'numeric' }); break; } }; diff --git a/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts b/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts index 938ce976e54..7024527de25 100644 --- a/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts +++ b/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts @@ -244,4 +244,87 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(page.locator('#time-button')).not.toBeVisible(); }); }); + + test.describe(title('datetime-button: formatOptions'), () => { + test('should include date and time for presentation date-time', async ({ page }) => { + await page.setContent( + ` + + + + `, + config + ); + + await page.locator('.datetime-ready').waitFor(); + + await expect(page.locator('#date-button')).toContainText('Thu, November 02'); + await expect(page.locator('#time-button')).toContainText('01:22 AM'); + }); + + test('should include date for presentation date', async ({ page }) => { + await page.setContent( + ` + + + + `, + config + ); + + await page.locator('.datetime-ready').waitFor(); + + await expect(page.locator('#date-button')).toContainText('Thu, November 02'); + }); + + test('should include date and time in same button for preferWheel', async ({ page }) => { + await page.setContent( + ` + + + + `, + config + ); + + await page.locator('.datetime-ready').waitFor(); + + await expect(page.locator('ion-datetime-button')).toContainText('Thu, November 02 01:22 AM'); + }); + }); }); diff --git a/core/src/components/datetime-button/test/basic/index.html b/core/src/components/datetime-button/test/basic/index.html index c446544a0a3..dd269d984ed 100644 --- a/core/src/components/datetime-button/test/basic/index.html +++ b/core/src/components/datetime-button/test/basic/index.html @@ -215,8 +215,41 @@

preferWheel / date-time

> + +
+

formatOptions

+ + + Start Date + + + + + + +
+ + diff --git a/core/src/components/datetime/datetime-interface.ts b/core/src/components/datetime/datetime-interface.ts index 255f39e22dc..475a672d069 100644 --- a/core/src/components/datetime/datetime-interface.ts +++ b/core/src/components/datetime/datetime-interface.ts @@ -36,3 +36,16 @@ export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle; export type DatetimeHighlightCallback = (dateIsoString: string) => DatetimeHighlightStyle | undefined; export type DatetimeHourCycle = 'h11' | 'h12' | 'h23' | 'h24'; + +/** + * FormatOptions must include date and/or time; it cannot be an empty object + */ +export type FormatOptions = + | { + date: Intl.DateTimeFormatOptions; + time?: Intl.DateTimeFormatOptions; + } + | { + date?: Intl.DateTimeFormatOptions; + time: Intl.DateTimeFormatOptions; + }; diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 57f275e25f0..f6a827d8e5d 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -20,6 +20,7 @@ import type { DatetimeHighlightStyle, DatetimeHighlightCallback, DatetimeHourCycle, + FormatOptions, } from './datetime-interface'; import { isSameDay, warnIfValueOutOfBounds, isBefore, isAfter } from './utils/comparison'; import { @@ -33,7 +34,7 @@ import { getTimeColumnsData, getCombinedDateColumnData, } from './utils/data'; -import { formatValue, getLocalizedTime, getMonthAndDay, getMonthAndYear } from './utils/format'; +import { formatValue, getLocalizedDateTime, getLocalizedTime, getMonthAndYear } from './utils/format'; import { isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth, getHourCycle } from './utils/helpers'; import { calculateHourFromAMPM, @@ -68,6 +69,7 @@ import { isNextMonthDisabled, isPrevMonthDisabled, } from './utils/state'; +import { checkForPresentationFormatMismatch, warnIfTimeZoneProvided } from './utils/validate'; /** * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. @@ -171,6 +173,20 @@ export class Datetime implements ComponentInterface { */ @Prop() disabled = false; + /** + * Formatting options for dates and times. + * Should include a 'date' and/or 'time' object, each of which is of type [Intl.DateTimeFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options). + * + */ + @Prop() formatOptions?: FormatOptions; + + @Watch('formatOptions') + protected formatOptionsChanged() { + const { el, formatOptions, presentation } = this; + checkForPresentationFormatMismatch(el, presentation, formatOptions); + warnIfTimeZoneProvided(el, formatOptions); + } + /** * If `true`, the datetime appears normal but the selected date cannot be changed. */ @@ -235,6 +251,12 @@ export class Datetime implements ComponentInterface { */ @Prop() presentation: DatetimePresentation = 'date-time'; + @Watch('presentation') + protected presentationChanged() { + const { el, formatOptions, presentation } = this; + checkForPresentationFormatMismatch(el, presentation, formatOptions); + } + private get isGridStyle() { const { presentation, preferWheel } = this; const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date'; @@ -1357,7 +1379,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 +1404,11 @@ export class Datetime implements ComponentInterface { } } + if (formatOptions) { + checkForPresentationFormatMismatch(el, presentation, formatOptions); + warnIfTimeZoneProvided(el, formatOptions); + } + const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues)); const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues)); const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues)); @@ -2354,7 +2381,7 @@ export class Datetime implements ComponentInterface { } private renderTimeOverlay() { - const { disabled, hourCycle, isTimePopoverOpen, locale } = this; + const { disabled, hourCycle, isTimePopoverOpen, locale, formatOptions } = this; const computedHourCycle = getHourCycle(locale, hourCycle); const activePart = this.getActivePartsWithFallback(); @@ -2389,7 +2416,7 @@ export class Datetime implements ComponentInterface { } }} > - {getLocalizedTime(locale, activePart, computedHourCycle)} + {getLocalizedTime(locale, activePart, computedHourCycle, formatOptions?.time)} , { }); }); }); + +/** + * This behavior does not differ across + * directions. + */ +configs({ directions: ['ltr'] }).forEach(({ title, 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 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'); + }); + }); +}); + +/** + * This behavior does not differ across + * modes/directions. + */ +configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('datetime: formatOptions misconfiguration errors'), () => { + test('should log 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 + ); + + await page.locator('.datetime-ready').waitFor(); + + expect(logs.length).toBe(1); + expect(logs[0]).toContain( + '[Ionic Warning]: Datetime: "timeZone" and "timeZoneName" are not supported in "formatOptions".' + ); + }); + + test('should log a warning if the required formatOptions are not provided for a presentation', async ({ page }) => { + const logs: string[] = []; + + page.on('console', (msg) => { + if (msg.type() === 'warning') { + logs.push(msg.text()); + } + }); + + await page.setContent( + ` + + Select Date + + + `, + config + ); + + await page.locator('.datetime-ready').waitFor(); + + expect(logs.length).toBe(1); + expect(logs[0]).toContain( + "[Ionic Warning]: Datetime: The 'date-time' presentation requires either a date or time object (or both) in formatOptions." + ); + }); + }); +}); diff --git a/core/src/components/datetime/test/basic/index.html b/core/src/components/datetime/test/basic/index.html index 2023b4a1190..b3afa766887 100644 --- a/core/src/components/datetime/test/basic/index.html +++ b/core/src/components/datetime/test/basic/index.html @@ -308,6 +308,13 @@

Modal - Custom

+ +
+

formatOptions

+ + Select Date + +
diff --git a/core/src/components/datetime/test/format.spec.ts b/core/src/components/datetime/test/format.spec.ts index 5ff218167d4..7161a1aeae3 100644 --- a/core/src/components/datetime/test/format.spec.ts +++ b/core/src/components/datetime/test/format.spec.ts @@ -1,12 +1,12 @@ import type { DatetimeParts } from '../datetime-interface'; import { generateDayAriaLabel, - getMonthAndDay, getFormattedHour, addTimePadding, getMonthAndYear, getLocalizedDayPeriod, getLocalizedTime, + stripTimeZone, } from '../utils/format'; describe('generateDayAriaLabel()', () => { @@ -37,24 +37,6 @@ describe('generateDayAriaLabel()', () => { }); }); -describe('getMonthAndDay()', () => { - it('should return Tue, May 11', () => { - expect(getMonthAndDay('en-US', { month: 5, day: 11, year: 2021 })).toEqual('Tue, May 11'); - }); - - it('should return mar, 11 may', () => { - expect(getMonthAndDay('es-ES', { month: 5, day: 11, year: 2021 })).toEqual('mar, 11 may'); - }); - - it('should return Sat, Apr 1', () => { - expect(getMonthAndDay('en-US', { month: 4, day: 1, year: 2006 })).toEqual('Sat, Apr 1'); - }); - - it('should return sáb, 1 abr', () => { - expect(getMonthAndDay('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('sáb, 1 abr'); - }); -}); - describe('getFormattedHour()', () => { it('should only add padding if using 24 hour time', () => { expect(getFormattedHour(1, 'h11')).toEqual('1'); @@ -144,6 +126,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 +136,79 @@ 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', + timeZoneName: 'long', + hour: 'numeric', + minute: 'numeric', + }; + + expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('9:40 AM'); + }); + + it('should not include time zone name', () => { + const datetimeParts: DatetimeParts = { + day: 1, + month: 1, + year: 2022, + hour: 9, + minute: 40, + }; + + const formatOptions: Intl.DateTimeFormatOptions = { + timeZone: 'America/Los_Angeles', + timeZoneName: 'long', + hour: 'numeric', + minute: 'numeric', + }; + + expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('9:40 AM'); + }); +}); + +describe('stripTimeZone', () => { + it('should remove the time zone name from the options and set the time zone to UTC', () => { + const formatOptions: Intl.DateTimeFormatOptions = { + timeZone: 'America/Los_Angeles', + timeZoneName: 'long', + hour: 'numeric', + minute: 'numeric', + }; + + expect(stripTimeZone(formatOptions)).toEqual({ + timeZone: 'UTC', + hour: 'numeric', + minute: 'numeric', + }); + }); }); diff --git a/core/src/components/datetime/utils/format.ts b/core/src/components/datetime/utils/format.ts index 0f70299dc52..6443e1b5465 100644 --- a/core/src/components/datetime/utils/format.ts +++ b/core/src/components/datetime/utils/format.ts @@ -11,7 +11,33 @@ const getFormattedDayPeriod = (dayPeriod?: string) => { return dayPeriod.toUpperCase(); }; -export const getLocalizedTime = (locale: string, refParts: DatetimeParts, hourCycle: DatetimeHourCycle): string => { +/** + * Including time zone options may lead to the rendered text showing a + * different time from what was selected in the Datetime, which could cause + * confusion. + */ +export const stripTimeZone = (formatOptions: Intl.DateTimeFormatOptions): Intl.DateTimeFormatOptions => { + return { + ...formatOptions, + /** + * Setting the time zone to UTC ensures that the value shown is always the + * same as what was selected and safeguards against older Safari bugs with + * Intl.DateTimeFormat. + */ + timeZone: 'UTC', + /** + * We do not want to display the time zone name + */ + timeZoneName: undefined, + }; +}; + +export const getLocalizedTime = ( + locale: string, + refParts: DatetimeParts, + hourCycle: DatetimeHourCycle, + formatOptions: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' } +): string => { const timeParts: Pick = { hour: refParts.hour, minute: refParts.minute, @@ -22,15 +48,7 @@ export const getLocalizedTime = (locale: string, refParts: DatetimeParts, hourCy } return new Intl.DateTimeFormat(locale, { - hour: 'numeric', - minute: 'numeric', - /** - * Setting the timeZone to UTC prevents - * new Intl.DatetimeFormat from subtracting - * the user's current timezone offset - * when formatting the time. - */ - timeZone: 'UTC', + ...stripTimeZone(formatOptions), /** * We use hourCycle here instead of hour12 due to: * https://bugs.chromium.org/p/chromium/issues/detail?id=1347316&q=hour12&can=2 @@ -146,17 +164,6 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D return today ? `Today, ${labelString}` : labelString; }; -/** - * Gets the day of the week, month, and day - * Used for the header in MD mode. - */ -export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => { - const date = getNormalizedDate(refParts); - return new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }).format( - date - ); -}; - /** * Given a locale and a date object, * return a formatted string that includes @@ -168,16 +175,6 @@ export const getMonthAndYear = (locale: string, refParts: DatetimeParts) => { return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date); }; -/** - * Given a locale and a date object, - * return a formatted string that includes - * the short month, numeric day, and full year. - * Example: Apr 22, 2021 - */ -export const getMonthDayAndYear = (locale: string, refParts: DatetimeParts) => { - return getLocalizedDateTime(locale, refParts, { month: 'short', day: 'numeric', year: 'numeric' }); -}; - /** * Given a locale and a date object, * return a formatted string that includes @@ -235,7 +232,7 @@ export const getLocalizedDateTime = ( options: Intl.DateTimeFormatOptions ): string => { const date = getNormalizedDate(refParts); - return getDateTimeFormat(locale, options).format(date); + return getDateTimeFormat(locale, stripTimeZone(options)).format(date); }; /** diff --git a/core/src/components/datetime/utils/validate.ts b/core/src/components/datetime/utils/validate.ts new file mode 100644 index 00000000000..46c3fe633b8 --- /dev/null +++ b/core/src/components/datetime/utils/validate.ts @@ -0,0 +1,54 @@ +import { printIonWarning } from '@utils/logging'; + +import type { DatetimePresentation, FormatOptions } from '../datetime-interface'; + +/** + * If a time zone is provided in the format options, the rendered text could + * differ from what was selected in the Datetime, which could cause + * confusion. + */ +export const warnIfTimeZoneProvided = (el: HTMLElement, formatOptions?: FormatOptions) => { + if ( + formatOptions?.date?.timeZone || + formatOptions?.date?.timeZoneName || + formatOptions?.time?.timeZone || + formatOptions?.time?.timeZoneName + ) { + printIonWarning('Datetime: "timeZone" and "timeZoneName" are not supported in "formatOptions".', el); + } +}; + +export const checkForPresentationFormatMismatch = ( + el: HTMLElement, + presentation: DatetimePresentation, + formatOptions?: FormatOptions +) => { + // formatOptions is not required + if (!formatOptions) return; + + // If formatOptions is provided, the date and/or time objects are required, depending on the presentation + switch (presentation) { + case 'date': + case 'month-year': + case 'month': + case 'year': + if (formatOptions.date === undefined) { + printIonWarning(`Datetime: The '${presentation}' presentation requires a date object in formatOptions.`, el); + } + break; + case 'time': + if (formatOptions.time === undefined) { + printIonWarning(`Datetime: The 'time' presentation requires a time object in formatOptions.`, el); + } + break; + case 'date-time': + case 'time-date': + if (formatOptions.date === undefined && formatOptions.time === undefined) { + printIonWarning( + `Datetime: The '${presentation}' presentation requires either a date or time object (or both) in formatOptions.`, + el + ); + } + break; + } +}; diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index be168ed8703..a592c55894b 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 1ea101fb239..57380e47bde 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -274,6 +274,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer