Skip to content

Commit 7cdbc1b

Browse files
mapsandappsIonitronliamdebeasi
authored
feat(datetime): formatOptions property for Datetime (#29065)
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, Datetime time button, and Datetime 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 - The developer can customize the date and time formatting for the Datetime Button - A warning will appear in the console if they try to provide a time zone (the time zone will not get used) - A warning will be logged if they do not include the date or time object for formatOptions as needed for the presentation of the Datetime ## 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. --> These changes have been reviewed in #29009 and #29059. This PR just adds them to the feature branch now that the separate tickets are complete. --------- Co-authored-by: ionitron <[email protected]> Co-authored-by: Liam DeBeasi <[email protected]>
1 parent 7033a28 commit 7cdbc1b

File tree

14 files changed

+478
-70
lines changed

14 files changed

+478
-70
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-button/datetime-button.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { getIonMode } from '../../global/ionic-global';
88
import type { Color } from '../../interface';
99
import type { DatetimePresentation } from '../datetime/datetime-interface';
1010
import { getToday } from '../datetime/utils/data';
11-
import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
11+
import { getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
1212
import { getHourCycle } from '../datetime/utils/helpers';
1313
import { parseDate } from '../datetime/utils/parse';
1414
/**
@@ -196,7 +196,7 @@ export class DatetimeButton implements ComponentInterface {
196196
return;
197197
}
198198

199-
const { value, locale, hourCycle, preferWheel, multiple, titleSelectedDatesFormatter } = datetimeEl;
199+
const { value, locale, formatOptions, hourCycle, preferWheel, multiple, titleSelectedDatesFormatter } = datetimeEl;
200200

201201
const parsedValues = this.getParsedDateValues(value);
202202

@@ -225,8 +225,12 @@ export class DatetimeButton implements ComponentInterface {
225225
switch (datetimePresentation) {
226226
case 'date-time':
227227
case 'time-date':
228-
const dateText = getMonthDayAndYear(locale, firstParsedDatetime);
229-
const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle);
228+
const dateText = getLocalizedDateTime(
229+
locale,
230+
firstParsedDatetime,
231+
formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' }
232+
);
233+
const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle, formatOptions?.time);
230234
if (preferWheel) {
231235
this.dateText = `${dateText} ${timeText}`;
232236
} else {
@@ -246,20 +250,28 @@ export class DatetimeButton implements ComponentInterface {
246250
}
247251
this.dateText = headerText;
248252
} else {
249-
this.dateText = getMonthDayAndYear(locale, firstParsedDatetime);
253+
this.dateText = getLocalizedDateTime(
254+
locale,
255+
firstParsedDatetime,
256+
formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' }
257+
);
250258
}
251259
break;
252260
case 'time':
253-
this.timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle);
261+
this.timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle, formatOptions?.time);
254262
break;
255263
case 'month-year':
256-
this.dateText = getMonthAndYear(locale, firstParsedDatetime);
264+
this.dateText = getLocalizedDateTime(
265+
locale,
266+
firstParsedDatetime,
267+
formatOptions?.date ?? { month: 'long', year: 'numeric' }
268+
);
257269
break;
258270
case 'month':
259-
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, { month: 'long' });
271+
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, formatOptions?.time ?? { month: 'long' });
260272
break;
261273
case 'year':
262-
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, { year: 'numeric' });
274+
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, formatOptions?.time ?? { year: 'numeric' });
263275
break;
264276
}
265277
};

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,87 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
244244
await expect(page.locator('#time-button')).not.toBeVisible();
245245
});
246246
});
247+
248+
test.describe(title('datetime-button: formatOptions'), () => {
249+
test('should include date and time for presentation date-time', async ({ page }) => {
250+
await page.setContent(
251+
`
252+
<ion-datetime-button datetime="datetime"></ion-datetime-button>
253+
<ion-datetime id="datetime" presentation="date-time" value="2023-11-02T01:22:00" locale="en-US"></ion-datetime>
254+
<script>
255+
const datetime = document.querySelector('ion-datetime');
256+
datetime.formatOptions = {
257+
date: {
258+
weekday: "short",
259+
month: "long",
260+
day: "2-digit"
261+
},
262+
time: {
263+
hour: "2-digit",
264+
minute: "2-digit"
265+
}
266+
}
267+
</script>
268+
`,
269+
config
270+
);
271+
272+
await page.locator('.datetime-ready').waitFor();
273+
274+
await expect(page.locator('#date-button')).toContainText('Thu, November 02');
275+
await expect(page.locator('#time-button')).toContainText('01:22 AM');
276+
});
277+
278+
test('should include date for presentation date', async ({ page }) => {
279+
await page.setContent(
280+
`
281+
<ion-datetime-button datetime="datetime"></ion-datetime-button>
282+
<ion-datetime id="datetime" presentation="date" value="2023-11-02" locale="en-US"></ion-datetime>
283+
<script>
284+
const datetime = document.querySelector('ion-datetime');
285+
datetime.formatOptions = {
286+
date: {
287+
weekday: "short",
288+
month: "long",
289+
day: "2-digit"
290+
}
291+
}
292+
</script>
293+
`,
294+
config
295+
);
296+
297+
await page.locator('.datetime-ready').waitFor();
298+
299+
await expect(page.locator('#date-button')).toContainText('Thu, November 02');
300+
});
301+
302+
test('should include date and time in same button for preferWheel', async ({ page }) => {
303+
await page.setContent(
304+
`
305+
<ion-datetime-button datetime="datetime"></ion-datetime-button>
306+
<ion-datetime id="datetime" presentation="date-time" value="2023-11-02T01:22:00" locale="en-US" prefer-wheel="true"></ion-datetime>
307+
<script>
308+
const datetime = document.querySelector('ion-datetime');
309+
datetime.formatOptions = {
310+
date: {
311+
weekday: "short",
312+
month: "long",
313+
day: "2-digit"
314+
},
315+
time: {
316+
hour: "2-digit",
317+
minute: "2-digit"
318+
}
319+
}
320+
</script>
321+
`,
322+
config
323+
);
324+
325+
await page.locator('.datetime-ready').waitFor();
326+
327+
await expect(page.locator('ion-datetime-button')).toContainText('Thu, November 02 01:22 AM');
328+
});
329+
});
247330
});

core/src/components/datetime-button/test/basic/index.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,41 @@ <h2>preferWheel / date-time</h2>
215215
></ion-datetime>
216216
</ion-popover>
217217
</div>
218+
219+
<div class="grid-item">
220+
<h2>formatOptions</h2>
221+
222+
<ion-item>
223+
<ion-label>Start Date</ion-label>
224+
<ion-datetime-button datetime="format-options" slot="end"></ion-datetime-button>
225+
</ion-item>
226+
227+
<ion-popover arrow="false">
228+
<ion-datetime
229+
id="format-options"
230+
presentation="date-time"
231+
value="2023-11-02T01:22:00"
232+
locale="en-US"
233+
></ion-datetime>
234+
</ion-popover>
235+
</div>
218236
</div>
219237
</ion-content>
220238
</ion-app>
221239
</body>
240+
241+
<script>
242+
const formatOptionsDatetime = document.querySelector('#format-options');
243+
formatOptionsDatetime.formatOptions = {
244+
date: {
245+
weekday: 'short',
246+
month: 'long',
247+
day: '2-digit',
248+
},
249+
time: {
250+
hour: '2-digit',
251+
minute: '2-digit',
252+
},
253+
};
254+
</script>
222255
</html>

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/validate';
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 { el, formatOptions, presentation } = this;
186+
checkForPresentationFormatMismatch(el, presentation, formatOptions);
187+
warnIfTimeZoneProvided(el, 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 { el, formatOptions, presentation } = this;
257+
checkForPresentationFormatMismatch(el, 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(el, presentation, formatOptions);
1409+
warnIfTimeZoneProvided(el, 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;

0 commit comments

Comments
 (0)