Skip to content

Commit

Permalink
feat(datetime): add custom timezone display property (#19519)
Browse files Browse the repository at this point in the history
resolves #19401
  • Loading branch information
daem0ndev authored and liamdebeasi committed Jan 9, 2020
1 parent 39d1262 commit 7b032c5
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 23 deletions.
4 changes: 2 additions & 2 deletions angular/src/directives/proxies.ts
Expand Up @@ -195,8 +195,8 @@ export class IonContent {
}

export declare interface IonDatetime extends Components.IonDatetime {}
@ProxyCmp({inputs: ['cancelText', 'dayNames', 'dayShortNames', 'dayValues', 'disabled', 'displayFormat', 'doneText', 'hourValues', 'max', 'min', 'minuteValues', 'mode', 'monthNames', 'monthShortNames', 'monthValues', 'name', 'pickerFormat', 'pickerOptions', 'placeholder', 'readonly', 'value', 'yearValues'], 'methods': ['open']})
@Component({ selector: 'ion-datetime', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['cancelText', 'dayNames', 'dayShortNames', 'dayValues', 'disabled', 'displayFormat', 'doneText', 'hourValues', 'max', 'min', 'minuteValues', 'mode', 'monthNames', 'monthShortNames', 'monthValues', 'name', 'pickerFormat', 'pickerOptions', 'placeholder', 'readonly', 'value', 'yearValues'] })
@ProxyCmp({inputs: ['cancelText', 'dayNames', 'dayShortNames', 'dayValues', 'disabled', 'displayFormat', 'displayTimezone', 'doneText', 'hourValues', 'max', 'min', 'minuteValues', 'mode', 'monthNames', 'monthShortNames', 'monthValues', 'name', 'pickerFormat', 'pickerOptions', 'placeholder', 'readonly', 'value', 'yearValues'], 'methods': ['open']})
@Component({ selector: 'ion-datetime', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['cancelText', 'dayNames', 'dayShortNames', 'dayValues', 'disabled', 'displayFormat', 'displayTimezone', 'doneText', 'hourValues', 'max', 'min', 'minuteValues', 'mode', 'monthNames', 'monthShortNames', 'monthValues', 'name', 'pickerFormat', 'pickerOptions', 'placeholder', 'readonly', 'value', 'yearValues'] })
export class IonDatetime {
ionCancel!: EventEmitter<CustomEvent>;
ionChange!: EventEmitter<CustomEvent>;
Expand Down
2 changes: 1 addition & 1 deletion core/CONTRIBUTING.md
Expand Up @@ -51,7 +51,7 @@ npm start

You should be able to navigate to `http://localhost:3333` which will look like a file browser.

E2E tests are located inside the `src/component` folder, in the following way: `http://localhost:3333/src/components/{COMPONENT}/test/`
E2E tests are located inside the `src/components` folder, in the following way: `http://localhost:3333/src/components/{COMPONENT}/test/`


**Path examples:**
Expand Down
1 change: 1 addition & 0 deletions core/api.txt
Expand Up @@ -301,6 +301,7 @@ ion-datetime,prop,dayShortNames,string | string[] | undefined,undefined,false,fa
ion-datetime,prop,dayValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,prop,disabled,boolean,false,false,false
ion-datetime,prop,displayFormat,string,'MMM D, YYYY',false,false
ion-datetime,prop,displayTimezone,string | undefined,undefined,false,false
ion-datetime,prop,doneText,string,'Done',false,false
ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,prop,max,string | undefined,undefined,false,false
Expand Down
8 changes: 8 additions & 0 deletions core/src/components.d.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
29 changes: 21 additions & 8 deletions core/src/components/datetime/datetime-util.ts
Expand Up @@ -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
Expand All @@ -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(),
Expand All @@ -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();
}
}

Expand Down
18 changes: 15 additions & 3 deletions core/src/components/datetime/datetime.tsx
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down
18 changes: 17 additions & 1 deletion core/src/components/datetime/readme.md
Expand Up @@ -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
Expand Down Expand Up @@ -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` |
Expand Down
21 changes: 19 additions & 2 deletions core/src/components/datetime/test/basic/index.html
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit 7b032c5

Please sign in to comment.