feat(datetime): add custom timezone display property (#19519)

resolves #19401
This commit is contained in:
Asif Rahman
2020-01-09 15:29:40 -05:00
committed by Liam DeBeasi
parent 39d12629db
commit 7b032c5e9b
9 changed files with 103 additions and 23 deletions

View File

@ -680,6 +680,10 @@ export namespace Components {
*/
'displayFormat': string;
/**
* The timezone to use for display purposes only. See [Date.prototype.toLocaleString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) for a list of supported timezones. If no value is provided, the component will default to displaying times in the user's local timezone.
*/
'displayTimezone'?: string;
/**
* The text to display on the picker's "Done" button.
*/
'doneText': string;
@ -4116,6 +4120,10 @@ declare namespace LocalJSX {
*/
'displayFormat'?: string;
/**
* The timezone to use for display purposes only. See [Date.prototype.toLocaleString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) for a list of supported timezones. If no value is provided, the component will default to displaying times in the user's local timezone.
*/
'displayTimezone'?: string;
/**
* The text to display on the picker's "Done" button.
*/
'doneText'?: string;

View File

@ -242,12 +242,13 @@ export const parseDate = (val: string | undefined | null): DatetimeData | undefi
};
/**
* Converts a valid UTC datetime string
* To the user's local timezone
* Converts a valid UTC datetime string to JS Date time object.
* By default uses the users local timezone, but an optional
* timezone can be provided.
* Note: This is not meant for time strings
* such as "01:47"
*/
export const getLocalDateTime = (dateString: any = ''): Date => {
export const getDateTime = (dateString: any = '', timeZone: any = ''): Date => {
/**
* If user passed in undefined
* or null, convert it to the
@ -273,7 +274,7 @@ export const getLocalDateTime = (dateString: any = ''): Date => {
}
const date = (typeof dateString === 'string' && dateString.length > 0) ? new Date(dateString) : new Date();
return new Date(
const localDateTime = new Date(
Date.UTC(
date.getFullYear(),
date.getMonth(),
@ -284,14 +285,26 @@ export const getLocalDateTime = (dateString: any = ''): Date => {
date.getMilliseconds()
)
);
if (timeZone && timeZone.length > 0) {
return new Date(date.getTime() - getTimezoneOffset(localDateTime, timeZone));
}
return localDateTime;
};
export const getTimezoneOffset = (localDate: Date, timeZone: string) => {
const utcDateTime = new Date(localDate.toLocaleString('en-US', { timeZone: 'utc' }));
const tzDateTime = new Date(localDate.toLocaleString('en-US', { timeZone }));
return utcDateTime.getTime() - tzDateTime.getTime();
};
export const updateDate = (existingData: DatetimeData, newData: any): boolean => {
export const updateDate = (existingData: DatetimeData, newData: any, displayTimezone?: string): boolean => {
if (!newData || typeof newData === 'string') {
const localDateTime = getLocalDateTime(newData);
if (!Number.isNaN(localDateTime.getTime())) {
newData = localDateTime.toISOString();
const dateTime = getDateTime(newData, displayTimezone);
if (!Number.isNaN(dateTime.getTime())) {
newData = dateTime.toISOString();
}
}

View File

@ -6,7 +6,7 @@ import { clamp, findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { pickerController } from '../../utils/overlays';
import { hostContext } from '../../utils/theme';
import { DatetimeData, LocaleData, convertDataToISO, convertFormatToKey, convertToArrayOfNumbers, convertToArrayOfStrings, dateDataSortValue, dateSortValue, dateValueRange, daysInMonth, getDateValue, parseDate, parseTemplate, renderDatetime, renderTextFormat, updateDate } from './datetime-util';
import { DatetimeData, LocaleData, convertDataToISO, convertFormatToKey, convertToArrayOfNumbers, convertToArrayOfStrings, dateDataSortValue, dateSortValue, dateValueRange, daysInMonth, getDateValue, getTimezoneOffset, parseDate, parseTemplate, renderDatetime, renderTextFormat, updateDate } from './datetime-util';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@ -81,6 +81,14 @@ export class Datetime implements ComponentInterface {
*/
@Prop() displayFormat = 'MMM D, YYYY';
/**
* The timezone to use for display purposes only. See
* [Date.prototype.toLocaleString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString)
* for a list of supported timezones. If no value is provided, the
* component will default to displaying times in the user's local timezone.
*/
@Prop() displayTimezone?: string;
/**
* The format of the date and time picker columns the user selects.
* A datetime input can have one or many datetime parts, each getting their
@ -287,7 +295,7 @@ export class Datetime implements ComponentInterface {
}
private updateDatetimeValue(value: any) {
updateDate(this.datetimeValue, value);
updateDate(this.datetimeValue, value, this.displayTimezone);
}
private generatePickerOptions(): PickerOptions {
@ -326,7 +334,11 @@ export class Datetime implements ComponentInterface {
* there can be 1 hr difference when dealing w/ DST
*/
const date = new Date(convertDataToISO(this.datetimeValue));
this.datetimeValue.tzOffset = date.getTimezoneOffset() * -1;
// If a custom display timezone is provided, use that tzOffset value instead
this.datetimeValue.tzOffset = (this.displayTimezone !== undefined && this.displayTimezone.length > 0)
? ((getTimezoneOffset(date, this.displayTimezone)) / 1000 / 60) * -1
: date.getTimezoneOffset() * -1;
this.value = convertDataToISO(this.datetimeValue);
}

View File

@ -58,9 +58,24 @@ above can be passed in to the display format in any combination.
| `YYYY, MMMM` | `2005, June` |
| `MMM DD, YYYY HH:mm` | `Jun 17, 2005 11:06` |
**Important**: `ion-datetime` will always display values relative to the user's timezone.
**Important**: `ion-datetime` will by default display values relative to the user's timezone.
Given a value of `09:00:00+01:00`, the datetime component will
display it as `04:00:00-04:00` for users in a `-04:00` timezone offset.
To change the display to use a different timezone, use the displayTimezone property described below.
### Display Timezone
The `displayTimezone` property allows you to change the default behavior
of displaying values relative to the user's local timezone. In addition to "UTC" valid
time zone values are determined by the browser, and in most cases follow the time zone names
of the [IANA time zone database](https://www.iana.org/time-zones), such as "Asia/Shanghai",
"Asia/Kolkata", "America/New_York". In the following example:
```html
<ion-datetime value="2019-10-01T15:43:40.394Z" display-timezone="utc"></ion-datetime>
```
The displayed value will not be converted and will be displayed as provided (UTC).
### Picker Format
@ -650,6 +665,7 @@ export const DateTimeExample: React.FC = () => (
| `dayValues` | `day-values` | Values used to create the list of selectable days. By default every day is shown for the given month. However, to control exactly which days of the month to display, the `dayValues` input can take a number, an array of numbers, or a string of comma separated numbers. Note that even if the array days have an invalid number for the selected month, like `31` in February, it will correctly not show days which are not valid for the selected month. | `number \| number[] \| string \| undefined` | `undefined` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the datetime. | `boolean` | `false` |
| `displayFormat` | `display-format` | The display format of the date and time as text that shows within the item. When the `pickerFormat` input is not used, then the `displayFormat` is used for both display the formatted text, and determining the datetime picker's columns. See the `pickerFormat` input description for more info. Defaults to `MMM D, YYYY`. | `string` | `'MMM D, YYYY'` |
| `displayTimezone` | `display-timezone` | The timezone to use for display purposes only. See [Date.prototype.toLocaleString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) for a list of supported timezones. If no value is provided, the component will default to displaying times in the user's local timezone. | `string \| undefined` | `undefined` |
| `doneText` | `done-text` | The text to display on the picker's "Done" button. | `string` | `'Done'` |
| `hourValues` | `hour-values` | Values used to create the list of selectable hours. By default the hour values range from `0` to `23` for 24-hour, or `1` to `12` for 12-hour. However, to control exactly which hours to display, the `hourValues` input can take a number, an array of numbers, or a string of comma separated numbers. | `number \| number[] \| string \| undefined` | `undefined` |
| `max` | `max` | The maximum datetime allowed. Value must be a date string following the [ISO 8601 datetime format standard](https://www.w3.org/TR/NOTE-datetime), `1996-12-19`. The format does not have to be specific to an exact datetime. For example, the maximum could just be the year, such as `1994`. Defaults to the end of this year. | `string \| undefined` | `undefined` |

View File

@ -31,12 +31,12 @@
<ion-label>Default</ion-label>
<ion-datetime></ion-datetime>
</ion-item>
<ion-item>
<ion-label position="floating">Default with floating label</ion-label>
<ion-datetime></ion-datetime>
</ion-item>
<ion-item>
<ion-label position="floating">Placeholder with floating label</ion-label>
<ion-datetime placeholder="Select a date"></ion-datetime>
@ -142,6 +142,23 @@
</ion-item>
</ion-list>
<ion-list>
<ion-item>
<ion-label>Display UTC 00:00 in Local Timezone (default behavior)</ion-label>
<ion-datetime display-format="MMM DD, YYYY HH:mm" value="2020-01-01T00:00:00Z"></ion-datetime>
</ion-item>
<ion-item>
<ion-label>Display UTC 00:00 in UTC (display-timezone = 'utc')</ion-label>
<ion-datetime display-format="MMM DD, YYYY HH:mm" value="2020-01-01T00:00:00Z" display-timezone="utc"></ion-datetime>
</ion-item>
<ion-item>
<ion-label>Display UTC 00:00 in US Pacific Time (display-timezone = 'America/Los_Angeles')</ion-label>
<ion-datetime display-format="MMM DD, YYYY HH:mm" value="2020-01-01T00:00:00Z" display-timezone="America/Los_Angeles"></ion-datetime>
</ion-item>
</ion-list>
<ion-list>
<ion-item>
<ion-label>HH:mm:ss</ion-label>

View File

@ -1,4 +1,4 @@
import { DatetimeData, daysInMonth, getDateValue, getLocalDateTime, renderDatetime } from '../datetime-util';
import { DatetimeData, daysInMonth, getDateValue, getDateTime, renderDatetime } from '../datetime-util';
describe('Datetime', () => {
describe('getDateValue()', () => {
@ -32,7 +32,7 @@ describe('Datetime', () => {
});
});
describe('getLocalDateTime()', () => {
describe('getDateTime()', () => {
it('should format a datetime string according to the local timezone', () => {
const dateStringTests = [
@ -44,7 +44,7 @@ describe('Datetime', () => {
];
dateStringTests.forEach(test => {
const convertToLocal = getLocalDateTime(test.input);
const convertToLocal = getDateTime(test.input);
const timeZoneOffset = convertToLocal.getTimezoneOffset() / 60;
const expectedDateString = test.expectedOutput.replace('%HOUR%', padNumber(test.expectedHourUTC - timeZoneOffset));
@ -65,19 +65,32 @@ describe('Datetime', () => {
];
dateStringTests.forEach(test => {
const convertToLocal = getLocalDateTime(test.input);
const convertToLocal = getDateTime(test.input);
expect(convertToLocal.toISOString()).toContain(test.expectedOutput);
});
});
it('should format a datetime string using provided timezone', () => {
const dateStringTests = [
{ displayTimezone: 'utc', input: `2019-03-02T12:00:00.000Z`, expectedOutput: `2019-03-02T12:00:00.000Z` },
{ displayTimezone: 'America/New_York', input: `2019-03-02T12:00:00.000Z`, expectedOutput: `2019-03-02T07:00:00.000Z` },
{ displayTimezone: 'Asia/Tokyo', input: `2019-03-02T12:00:00.000Z`, expectedOutput: `2019-03-02T21:00:00.000Z` },
];
dateStringTests.forEach(test => {
const convertToLocal = getDateTime(test.input, test.displayTimezone);
expect(convertToLocal.toISOString()).toEqual(test.expectedOutput);
});
});
it('should default to today for null and undefined cases', () => {
const today = new Date();
const todayString = renderDatetime('YYYY-MM-DD', { year: today.getFullYear(), month: today.getMonth() + 1, day: today.getDate() } )
const convertToLocalUndefined = getLocalDateTime(undefined);
const convertToLocalUndefined = getDateTime(undefined);
expect(convertToLocalUndefined.toISOString()).toContain(todayString);
const convertToLocalNull = getLocalDateTime(null);
const convertToLocalNull = getDateTime(null);
expect(convertToLocalNull.toISOString()).toContain(todayString);
});
});