Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
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 <hi@ionicframework.com>
  • Loading branch information
mapsandapps and Ionitron authored Feb 15, 2024
commit 7ac08bca1e252a12a1c3d2cdc1a298bba1f8963d
1 change: 1 addition & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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"`.
*/
Expand Down Expand Up @@ -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"`.
*/
Expand Down
13 changes: 13 additions & 0 deletions core/src/components/datetime/datetime-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
43 changes: 37 additions & 6 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
DatetimeHighlightStyle,
DatetimeHighlightCallback,
DatetimeHourCycle,
FormatOptions,
} from './datetime-interface';
import { isSameDay, warnIfValueOutOfBounds, isBefore, isAfter } from './utils/comparison';
import {
Expand All @@ -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,
Expand Down Expand Up @@ -68,6 +69,7 @@ import {
isNextMonthDisabled,
isPrevMonthDisabled,
} from './utils/state';
import { checkForPresentationFormatMismatch, warnIfTimeZoneProvided } from './utils/warn';

/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
Expand Down Expand Up @@ -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 { formatOptions, presentation } = this;
checkForPresentationFormatMismatch(presentation, formatOptions);
warnIfTimeZoneProvided(formatOptions);
}

/**
* If `true`, the datetime appears normal but the selected date cannot be changed.
*/
Expand Down Expand Up @@ -235,6 +251,12 @@ export class Datetime implements ComponentInterface {
*/
@Prop() presentation: DatetimePresentation = 'date-time';

@Watch('presentation')
protected presentationChanged() {
const { formatOptions, presentation } = this;
checkForPresentationFormatMismatch(presentation, formatOptions);
}

private get isGridStyle() {
const { presentation, preferWheel } = this;
const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
Expand Down Expand Up @@ -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') {
Expand All @@ -1382,6 +1404,11 @@ export class Datetime implements ComponentInterface {
}
}

if (formatOptions) {
checkForPresentationFormatMismatch(presentation, formatOptions);
warnIfTimeZoneProvided(formatOptions);
}

const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues));
const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues));
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -2389,7 +2416,7 @@ export class Datetime implements ComponentInterface {
}
}}
>
{getLocalizedTime(locale, activePart, computedHourCycle)}
{getLocalizedTime(locale, activePart, computedHourCycle, formatOptions?.time)}
</button>,
<ion-popover
alignment="center"
Expand Down Expand Up @@ -2424,7 +2451,7 @@ export class Datetime implements ComponentInterface {
}

private getHeaderSelectedDateText() {
const { activeParts, multiple, titleSelectedDatesFormatter } = this;
const { activeParts, formatOptions, multiple, titleSelectedDatesFormatter } = this;
const isArray = Array.isArray(activeParts);

let headerText: string;
Expand All @@ -2439,7 +2466,11 @@ export class Datetime implements ComponentInterface {
}
} else {
// for exactly 1 day selected (multiple set or not), show a formatted version of that
headerText = getMonthAndDay(this.locale, this.getActivePartsWithFallback());
headerText = getLocalizedDateTime(
this.locale,
this.getActivePartsWithFallback(),
formatOptions?.date ?? { weekday: 'short', month: 'short', day: 'numeric' }
);
}

return headerText;
Expand Down
104 changes: 104 additions & 0 deletions core/src/components/datetime/test/basic/datetime.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,3 +565,107 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
});
});
});

/**
* 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(
`
<ion-datetime value="2022-02-01T16:30:00">
<span slot="title">Select Date</span>
</ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
time: { hour: '2-digit', minute: '2-digit' },
date: { day: '2-digit', month: 'long', era: 'short' },
}
</script>
`,
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(
`
<ion-datetime value="2022-02-01T16:30:00" presentation="date">
<span slot="title">Select Date</span>
</ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
date: { timeZone: 'UTC' },
}
</script>
`,
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(
`
<ion-datetime value="2022-02-01T16:30:00">
<span slot="title">Select Date</span>
</ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {}
</script>
`,
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."
);
});
});
});
Loading