Skip to content

Commit

Permalink
feat(datetime): formatOptions property for Datetime (#29065)
Browse files Browse the repository at this point in the history
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 <hi@ionicframework.com>
Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 22, 2024
1 parent 7033a28 commit 7cdbc1b
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 70 deletions.
1 change: 1 addition & 0 deletions core/api.txt
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
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
30 changes: 21 additions & 9 deletions core/src/components/datetime-button/datetime-button.tsx
Expand Up @@ -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';
/**
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}
};
Expand Down
Expand Up @@ -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(
`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time" value="2023-11-02T01:22:00" locale="en-US"></ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
date: {
weekday: "short",
month: "long",
day: "2-digit"
},
time: {
hour: "2-digit",
minute: "2-digit"
}
}
</script>
`,
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(
`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date" value="2023-11-02" locale="en-US"></ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
date: {
weekday: "short",
month: "long",
day: "2-digit"
}
}
</script>
`,
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(
`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time" value="2023-11-02T01:22:00" locale="en-US" prefer-wheel="true"></ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
date: {
weekday: "short",
month: "long",
day: "2-digit"
},
time: {
hour: "2-digit",
minute: "2-digit"
}
}
</script>
`,
config
);

await page.locator('.datetime-ready').waitFor();

await expect(page.locator('ion-datetime-button')).toContainText('Thu, November 02 01:22 AM');
});
});
});
33 changes: 33 additions & 0 deletions core/src/components/datetime-button/test/basic/index.html
Expand Up @@ -215,8 +215,41 @@ <h2>preferWheel / date-time</h2>
></ion-datetime>
</ion-popover>
</div>

<div class="grid-item">
<h2>formatOptions</h2>

<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button datetime="format-options" slot="end"></ion-datetime-button>
</ion-item>

<ion-popover arrow="false">
<ion-datetime
id="format-options"
presentation="date-time"
value="2023-11-02T01:22:00"
locale="en-US"
></ion-datetime>
</ion-popover>
</div>
</div>
</ion-content>
</ion-app>
</body>

<script>
const formatOptionsDatetime = document.querySelector('#format-options');
formatOptionsDatetime.formatOptions = {
date: {
weekday: 'short',
month: 'long',
day: '2-digit',
},
time: {
hour: '2-digit',
minute: '2-digit',
},
};
</script>
</html>
13 changes: 13 additions & 0 deletions core/src/components/datetime/datetime-interface.ts
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
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/validate';

/**
* @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 { el, formatOptions, presentation } = this;
checkForPresentationFormatMismatch(el, presentation, formatOptions);
warnIfTimeZoneProvided(el, 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 { 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';
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(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));
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

0 comments on commit 7cdbc1b

Please sign in to comment.