Skip to content

Commit 7ac08bc

Browse files
feat(datetime): formatOptions for time button and header (#29009)
Issue number: Internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> The Datetime header and time button have default date formatting that cannot be set by the developer. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - The developer can customize the date and time formatting for the Datetime header and time button - A warning will appear in the console if they try to provide a time zone (the time zone will not get used) ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer for more information. --> <!-- ## Other information Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> --------- Co-authored-by: ionitron <[email protected]>
1 parent 7033a28 commit 7ac08bc

File tree

10 files changed

+325
-50
lines changed

10 files changed

+325
-50
lines changed

core/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ ion-datetime,prop,dayValues,number | number[] | string | undefined,undefined,fal
394394
ion-datetime,prop,disabled,boolean,false,false,false
395395
ion-datetime,prop,doneText,string,'Done',false,false
396396
ion-datetime,prop,firstDayOfWeek,number,0,false,false
397+
ion-datetime,prop,formatOptions,undefined | { date: DateTimeFormatOptions; time?: DateTimeFormatOptions | undefined; } | { date?: DateTimeFormatOptions | undefined; time: DateTimeFormatOptions; },undefined,false,false
397398
ion-datetime,prop,highlightedDates,((dateIsoString: string) => DatetimeHighlightStyle | undefined) | DatetimeHighlight[] | undefined,undefined,false,false
398399
ion-datetime,prop,hourCycle,"h11" | "h12" | "h23" | "h24" | undefined,undefined,false,false
399400
ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false

core/src/components.d.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
1515
import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
1616
import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
1717
import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
18-
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
18+
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
1919
import { SpinnerTypes } from "./components/spinner/spinner-configs";
2020
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
2121
import { CounterFormatter } from "./components/item/item-interface";
@@ -51,7 +51,7 @@ export { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
5151
export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
5252
export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
5353
export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
54-
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
54+
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
5555
export { SpinnerTypes } from "./components/spinner/spinner-configs";
5656
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
5757
export { CounterFormatter } from "./components/item/item-interface";
@@ -858,6 +858,10 @@ export namespace Components {
858858
* The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday.
859859
*/
860860
"firstDayOfWeek": number;
861+
/**
862+
* 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).
863+
*/
864+
"formatOptions"?: FormatOptions;
861865
/**
862866
* 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"`.
863867
*/
@@ -5541,6 +5545,10 @@ declare namespace LocalJSX {
55415545
* The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday.
55425546
*/
55435547
"firstDayOfWeek"?: number;
5548+
/**
5549+
* 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).
5550+
*/
5551+
"formatOptions"?: FormatOptions;
55445552
/**
55455553
* 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"`.
55465554
*/

core/src/components/datetime/datetime-interface.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,16 @@ export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;
3636
export type DatetimeHighlightCallback = (dateIsoString: string) => DatetimeHighlightStyle | undefined;
3737

3838
export type DatetimeHourCycle = 'h11' | 'h12' | 'h23' | 'h24';
39+
40+
/**
41+
* FormatOptions must include date and/or time; it cannot be an empty object
42+
*/
43+
export type FormatOptions =
44+
| {
45+
date: Intl.DateTimeFormatOptions;
46+
time?: Intl.DateTimeFormatOptions;
47+
}
48+
| {
49+
date?: Intl.DateTimeFormatOptions;
50+
time: Intl.DateTimeFormatOptions;
51+
};

core/src/components/datetime/datetime.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
DatetimeHighlightStyle,
2121
DatetimeHighlightCallback,
2222
DatetimeHourCycle,
23+
FormatOptions,
2324
} from './datetime-interface';
2425
import { isSameDay, warnIfValueOutOfBounds, isBefore, isAfter } from './utils/comparison';
2526
import {
@@ -33,7 +34,7 @@ import {
3334
getTimeColumnsData,
3435
getCombinedDateColumnData,
3536
} from './utils/data';
36-
import { formatValue, getLocalizedTime, getMonthAndDay, getMonthAndYear } from './utils/format';
37+
import { formatValue, getLocalizedDateTime, getLocalizedTime, getMonthAndYear } from './utils/format';
3738
import { isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth, getHourCycle } from './utils/helpers';
3839
import {
3940
calculateHourFromAMPM,
@@ -68,6 +69,7 @@ import {
6869
isNextMonthDisabled,
6970
isPrevMonthDisabled,
7071
} from './utils/state';
72+
import { checkForPresentationFormatMismatch, warnIfTimeZoneProvided } from './utils/warn';
7173

7274
/**
7375
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@@ -171,6 +173,20 @@ export class Datetime implements ComponentInterface {
171173
*/
172174
@Prop() disabled = false;
173175

176+
/**
177+
* Formatting options for dates and times.
178+
* 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).
179+
*
180+
*/
181+
@Prop() formatOptions?: FormatOptions;
182+
183+
@Watch('formatOptions')
184+
protected formatOptionsChanged() {
185+
const { formatOptions, presentation } = this;
186+
checkForPresentationFormatMismatch(presentation, formatOptions);
187+
warnIfTimeZoneProvided(formatOptions);
188+
}
189+
174190
/**
175191
* If `true`, the datetime appears normal but the selected date cannot be changed.
176192
*/
@@ -235,6 +251,12 @@ export class Datetime implements ComponentInterface {
235251
*/
236252
@Prop() presentation: DatetimePresentation = 'date-time';
237253

254+
@Watch('presentation')
255+
protected presentationChanged() {
256+
const { formatOptions, presentation } = this;
257+
checkForPresentationFormatMismatch(presentation, formatOptions);
258+
}
259+
238260
private get isGridStyle() {
239261
const { presentation, preferWheel } = this;
240262
const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
@@ -1357,7 +1379,7 @@ export class Datetime implements ComponentInterface {
13571379
};
13581380

13591381
componentWillLoad() {
1360-
const { el, highlightedDates, multiple, presentation, preferWheel } = this;
1382+
const { el, formatOptions, highlightedDates, multiple, presentation, preferWheel } = this;
13611383

13621384
if (multiple) {
13631385
if (presentation !== 'date') {
@@ -1382,6 +1404,11 @@ export class Datetime implements ComponentInterface {
13821404
}
13831405
}
13841406

1407+
if (formatOptions) {
1408+
checkForPresentationFormatMismatch(presentation, formatOptions);
1409+
warnIfTimeZoneProvided(formatOptions);
1410+
}
1411+
13851412
const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
13861413
const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues));
13871414
const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues));
@@ -2354,7 +2381,7 @@ export class Datetime implements ComponentInterface {
23542381
}
23552382

23562383
private renderTimeOverlay() {
2357-
const { disabled, hourCycle, isTimePopoverOpen, locale } = this;
2384+
const { disabled, hourCycle, isTimePopoverOpen, locale, formatOptions } = this;
23582385
const computedHourCycle = getHourCycle(locale, hourCycle);
23592386
const activePart = this.getActivePartsWithFallback();
23602387

@@ -2389,7 +2416,7 @@ export class Datetime implements ComponentInterface {
23892416
}
23902417
}}
23912418
>
2392-
{getLocalizedTime(locale, activePart, computedHourCycle)}
2419+
{getLocalizedTime(locale, activePart, computedHourCycle, formatOptions?.time)}
23932420
</button>,
23942421
<ion-popover
23952422
alignment="center"
@@ -2424,7 +2451,7 @@ export class Datetime implements ComponentInterface {
24242451
}
24252452

24262453
private getHeaderSelectedDateText() {
2427-
const { activeParts, multiple, titleSelectedDatesFormatter } = this;
2454+
const { activeParts, formatOptions, multiple, titleSelectedDatesFormatter } = this;
24282455
const isArray = Array.isArray(activeParts);
24292456

24302457
let headerText: string;
@@ -2439,7 +2466,11 @@ export class Datetime implements ComponentInterface {
24392466
}
24402467
} else {
24412468
// for exactly 1 day selected (multiple set or not), show a formatted version of that
2442-
headerText = getMonthAndDay(this.locale, this.getActivePartsWithFallback());
2469+
headerText = getLocalizedDateTime(
2470+
this.locale,
2471+
this.getActivePartsWithFallback(),
2472+
formatOptions?.date ?? { weekday: 'short', month: 'short', day: 'numeric' }
2473+
);
24432474
}
24442475

24452476
return headerText;

core/src/components/datetime/test/basic/datetime.e2e.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,107 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
565565
});
566566
});
567567
});
568+
569+
/**
570+
* This behavior does not differ across
571+
* directions.
572+
*/
573+
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
574+
test.describe(title('datetime: formatOptions'), () => {
575+
test('should format header and time button', async ({ page }) => {
576+
await page.setContent(
577+
`
578+
<ion-datetime value="2022-02-01T16:30:00">
579+
<span slot="title">Select Date</span>
580+
</ion-datetime>
581+
<script>
582+
const datetime = document.querySelector('ion-datetime');
583+
datetime.formatOptions = {
584+
time: { hour: '2-digit', minute: '2-digit' },
585+
date: { day: '2-digit', month: 'long', era: 'short' },
586+
}
587+
</script>
588+
`,
589+
config
590+
);
591+
592+
await page.locator('.datetime-ready').waitFor();
593+
594+
const headerDate = page.locator('ion-datetime .datetime-selected-date');
595+
await expect(headerDate).toHaveText('February 01 AD');
596+
597+
const timeBody = page.locator('ion-datetime .time-body');
598+
await expect(timeBody).toHaveText('04:30 PM');
599+
});
600+
});
601+
});
602+
603+
/**
604+
* This behavior does not differ across
605+
* modes/directions.
606+
*/
607+
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
608+
test.describe(title('datetime: formatOptions misconfiguration errors'), () => {
609+
test('should log a warning if time zone is provided', async ({ page }) => {
610+
const logs: string[] = [];
611+
612+
page.on('console', (msg) => {
613+
if (msg.type() === 'warning') {
614+
logs.push(msg.text());
615+
}
616+
});
617+
618+
await page.setContent(
619+
`
620+
<ion-datetime value="2022-02-01T16:30:00" presentation="date">
621+
<span slot="title">Select Date</span>
622+
</ion-datetime>
623+
<script>
624+
const datetime = document.querySelector('ion-datetime');
625+
datetime.formatOptions = {
626+
date: { timeZone: 'UTC' },
627+
}
628+
</script>
629+
`,
630+
config
631+
);
632+
633+
await page.locator('.datetime-ready').waitFor();
634+
635+
expect(logs.length).toBe(1);
636+
expect(logs[0]).toContain(
637+
'[Ionic Warning]: Datetime: "timeZone" and "timeZoneName" are not supported in "formatOptions".'
638+
);
639+
});
640+
641+
test('should log a warning if the required formatOptions are not provided for a presentation', async ({ page }) => {
642+
const logs: string[] = [];
643+
644+
page.on('console', (msg) => {
645+
if (msg.type() === 'warning') {
646+
logs.push(msg.text());
647+
}
648+
});
649+
650+
await page.setContent(
651+
`
652+
<ion-datetime value="2022-02-01T16:30:00">
653+
<span slot="title">Select Date</span>
654+
</ion-datetime>
655+
<script>
656+
const datetime = document.querySelector('ion-datetime');
657+
datetime.formatOptions = {}
658+
</script>
659+
`,
660+
config
661+
);
662+
663+
await page.locator('.datetime-ready').waitFor();
664+
665+
expect(logs.length).toBe(1);
666+
expect(logs[0]).toContain(
667+
"[Ionic Warning]: Datetime: The 'date-time' presentation requires either a date or time object (or both) in formatOptions."
668+
);
669+
});
670+
});
671+
});

0 commit comments

Comments
 (0)