Skip to content

feat(datetime): add hourCycle property #23686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ export class IonContent {
}
export declare interface IonDatetime extends Components.IonDatetime {
}
@ProxyCmp({ inputs: ["cancelText", "color", "dayValues", "disabled", "doneText", "hourValues", "locale", "max", "min", "minuteValues", "mode", "monthValues", "name", "presentation", "readonly", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "value", "yearValues"], "methods": ["confirm", "reset", "cancel"] })
@Component({ selector: "ion-datetime", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["cancelText", "color", "dayValues", "disabled", "doneText", "hourValues", "locale", "max", "min", "minuteValues", "mode", "monthValues", "name", "presentation", "readonly", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "value", "yearValues"] })
@ProxyCmp({ inputs: ["cancelText", "color", "dayValues", "disabled", "doneText", "hourCycle", "hourValues", "locale", "max", "min", "minuteValues", "mode", "monthValues", "name", "presentation", "readonly", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "value", "yearValues"], "methods": ["confirm", "reset", "cancel"] })
@Component({ selector: "ion-datetime", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["cancelText", "color", "dayValues", "disabled", "doneText", "hourCycle", "hourValues", "locale", "max", "min", "minuteValues", "mode", "monthValues", "name", "presentation", "readonly", "showDefaultButtons", "showDefaultTimeLabel", "showDefaultTitle", "size", "value", "yearValues"] })
export class IonDatetime {
ionCancel!: EventEmitter<CustomEvent>;
ionChange!: EventEmitter<CustomEvent>;
Expand Down
1 change: 1 addition & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ ion-datetime,prop,color,string | undefined,'primary',false,false
ion-datetime,prop,dayValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,prop,disabled,boolean,false,false,false
ion-datetime,prop,doneText,string,'Done',false,false
ion-datetime,prop,hourCycle,"h12" | "h23" | undefined,undefined,false,false
ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,prop,locale,string,'default',false,false
ion-datetime,prop,max,string | undefined,undefined,false,false
Expand Down
8 changes: 8 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,10 @@ export namespace Components {
* The text to display on the picker's "Done" button.
*/
"doneText": string;
/**
* The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale.
*/
"hourCycle"?: 'h23' | 'h12';
/**
* 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.
*/
Expand Down Expand Up @@ -4292,6 +4296,10 @@ declare namespace LocalJSX {
* The text to display on the picker's "Done" button.
*/
"doneText"?: string;
/**
* The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale.
*/
"hourCycle"?: 'h23' | 'h12';
/**
* 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.
*/
Expand Down
11 changes: 9 additions & 2 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,12 @@ export class Datetime implements ComponentInterface {
*/
@Prop() showDefaultTimeLabel = true;

/**
* The hour cycle of the `ion-datetime`. If no value is set, this is
* specified by the current locale.
*/
@Prop() hourCycle?: 'h23' | 'h12';

/**
* If `cover`, the `ion-datetime` will expand to cover the full width of its container.
* If `fixed`, the `ion-datetime` will have a fixed width.
Expand Down Expand Up @@ -1397,9 +1403,10 @@ export class Datetime implements ComponentInterface {
* should just be the default segment.
*/
private renderTime(mode: Mode) {
const use24Hour = is24Hour(this.locale);
const { hourCycle } = this;
const use24Hour = is24Hour(this.locale, hourCycle);
const { ampm } = this.workingParts;
const { hours, minutes, am, pm } = generateTime(this.locale, this.workingParts, this.minParts, this.maxParts, this.parsedHourValues, this.parsedMinuteValues);
const { hours, minutes, am, pm } = generateTime(this.workingParts, use24Hour ? 'h23' : 'h12', this.minParts, this.maxParts, this.parsedHourValues, this.parsedMinuteValues);
return (
<div class="datetime-time">
<div class="time-header">
Expand Down
49 changes: 49 additions & 0 deletions core/src/components/datetime/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,39 @@ For instances where you need a specific locale, you can use the `locale` propert
<ion-datetime locale="fr-FR"></ion-datetime>
```

### Controlling the Hour Cycle

`ion-datetime` will use the hour cycle that is specified by the `locale` property by default. For example, if `locale` is set to `en-US`, then `ion-datetime` will use a 12 hour cycle.

There are 4 primary hour cycle types:

| Hour cycle type | Description |
| --------------- | ------------------------------------------------------------ |
| `'h12` | Hour system using 1–12; corresponds to 'h' in patterns. The 12 hour clock, with midnight starting at 12:00 am. |
| `'h23'` | Hour system using 0–23; corresponds to 'H' in patterns. The 24 hour clock, with midnight starting at 0:00. |
| `'h11'` | Hour system using 0–11; corresponds to 'K' in patterns. The 12 hour clock, with midnight starting at 0:00 am. |
| `'h24'` | Hour system using 1–24; corresponds to 'k' in pattern. The 24 hour clock, with midnight starting at 24:00. |

> Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle

There may be scenarios where you need to have more control over which hour cycle is used. This is where the `hour-cycle` property can help.

In the following example, we can use the `hour-cycle` property to force `ion-datetime` to use the 12 hour cycle even though the locale is `en-GB`, which uses a 24 hour cycle by default:

```html
<ion-datetime hour-cycle="h12" locale="en-GB"></ion-datetime>
```

`ion-datetime` also supports [locale extension tags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale). These tags let you encode information about the locale in the locale string itself. Developers may prefer to use the extension tag approach if they are using the [Intl.Locale API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) in their apps.

For example, if you wanted to use a 12 hour cycle with the `en-GB` locale, you could alternatively do:

```html
<ion-datetime locale="en-GB-u-hc-h12"></ion-datetime>
```

`ion-datetime` currently supports the `h12` and `h23` hour cycle types. Interested in seeing support for `h11` and `h24` added to `ion-datetime`? [Let us know!](https://github.com/ionic-team/ionic-framework/issues/23750)

## Parsing Dates

When `ionChange` is emitted, we provide an ISO-8601 string in the event payload. From there, it is the developer's responsibility to format it as they see fit. We recommend using a library like [date-fns](https://date-fns.org) to format their dates properly.
Expand Down Expand Up @@ -176,6 +209,9 @@ dates in JavaScript.
<!-- Full width size -->
<ion-datetime size="cover"></ion-datetime>

<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
<div slot="title">My Custom Title</div>
Expand Down Expand Up @@ -258,6 +294,9 @@ export class MyComponent {
<!-- Full width size -->
<ion-datetime size="cover"></ion-datetime>

<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
<div slot="title">My Custom Title</div>
Expand Down Expand Up @@ -352,6 +391,9 @@ export const DateTimeExamples: React.FC = () => {
{/* Full width size */}
<IonDatetime size="cover"></IonDatetime>

{/* Custom Hour Cycle */}
<IonDatetime hourCycle="h23"></IonDatetime>

{/* Custom title */}
<IonDatetime>
<div slot="title">My Custom Title</div>
Expand Down Expand Up @@ -436,6 +478,9 @@ export class DatetimeExample {
{/* Full width size */}
<ion-datetime size="cover"></ion-datetime>,

{/* Custom Hour Cycle */}
<ion-datetime hourCycle="h23"></ion-datetime>,

{/* Custom title */}
<ion-datetime>
<div slot="title">My Custom Title</div>
Expand Down Expand Up @@ -496,6 +541,9 @@ export class DatetimeExample {
<!-- Full width size -->
<ion-datetime size="cover"></ion-datetime>

<!-- Custom Hour Cycle -->
<ion-datetime hour-cycle="h23"></ion-datetime>

<!-- Custom title -->
<ion-datetime>
<div slot="title">My Custom Title</div>
Expand Down Expand Up @@ -569,6 +617,7 @@ export class DatetimeExample {
| `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` |
| `doneText` | `done-text` | The text to display on the picker's "Done" button. | `string` | `'Done'` |
| `hourCycle` | `hour-cycle` | The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale. | `"h12" \| "h23" \| undefined` | `undefined` |
| `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` |
| `locale` | `locale` | The locale to use for `ion-datetime`. This impacts month and day name formatting. The `'default'` value refers to the default locale set by your device. | `string` | `'default'` |
| `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
26 changes: 9 additions & 17 deletions core/src/components/datetime/test/data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@ describe('generateTime()', () => {
hour: 5,
minute: 43
}
const { hours, minutes, use24Hour } = generateTime('en-US', today);
const { hours, minutes } = generateTime(today);

expect(hours.length).toEqual(12);
expect(minutes.length).toEqual(60);
expect(use24Hour).toEqual(false);
});
it('should filter according to min', () => {
const today = {
Expand All @@ -62,11 +61,10 @@ describe('generateTime()', () => {
hour: 2,
minute: 40
}
const { hours, minutes, use24Hour } = generateTime('en-US', today, min);
const { hours, minutes } = generateTime(today, false, min);

expect(hours.length).toEqual(11);
expect(minutes.length).toEqual(20);
expect(use24Hour).toEqual(false);
})
it('should not filter according to min if not on reference day', () => {
const today = {
Expand All @@ -83,11 +81,10 @@ describe('generateTime()', () => {
hour: 2,
minute: 40
}
const { hours, minutes, use24Hour } = generateTime('en-US', today, min);
const { hours, minutes } = generateTime(today, 'h12', min);

expect(hours.length).toEqual(12);
expect(minutes.length).toEqual(60);
expect(use24Hour).toEqual(false);
})
it('should filter according to max', () => {
const today = {
Expand All @@ -104,11 +101,10 @@ describe('generateTime()', () => {
hour: 7,
minute: 44
}
const { hours, minutes, use24Hour } = generateTime('en-US', today, undefined, max);
const { hours, minutes } = generateTime(today, 'h12', undefined, max);

expect(hours.length).toEqual(7);
expect(minutes.length).toEqual(45);
expect(use24Hour).toEqual(false);
})
it('should not filter according to min if not on reference day', () => {
const today = {
Expand All @@ -125,11 +121,10 @@ describe('generateTime()', () => {
hour: 2,
minute: 40
}
const { hours, minutes, use24Hour } = generateTime('en-US', today, undefined, max);
const { hours, minutes } = generateTime(today, 'h12', undefined, max);

expect(hours.length).toEqual(12);
expect(minutes.length).toEqual(60);
expect(use24Hour).toEqual(false);
})
it('should return no values for a day less than the min', () => {
const today = {
Expand All @@ -146,11 +141,10 @@ describe('generateTime()', () => {
hour: 2,
minute: 40
}
const { hours, minutes, use24Hour } = generateTime('en-US', today, min);
const { hours, minutes } = generateTime(today, 'h12', min);

expect(hours.length).toEqual(0);
expect(minutes.length).toEqual(0);
expect(use24Hour).toEqual(false);
})
it('should return no values for a day greater than the max', () => {
const today = {
Expand All @@ -167,11 +161,10 @@ describe('generateTime()', () => {
hour: 2,
minute: 40
}
const { hours, minutes, use24Hour } = generateTime('en-US', today, undefined, max);
const { hours, minutes } = generateTime(today, 'h12', undefined, max);

expect(hours.length).toEqual(0);
expect(minutes.length).toEqual(0);
expect(use24Hour).toEqual(false);
})
it('should allow all hours and minutes if not set in min/max', () => {
const today = {
Expand All @@ -192,11 +185,10 @@ describe('generateTime()', () => {
year: 2021
}

const { hours, minutes, use24Hour } = generateTime('en-US', today, min, max);
const { hours, minutes } = generateTime(today, 'h12', min, max);

expect(hours.length).toEqual(12);
expect(minutes.length).toEqual(60);
expect(use24Hour).toEqual(false);
})
it('should allow certain hours and minutes based on minuteValues and hourValues', () => {
const today = {
Expand All @@ -207,7 +199,7 @@ describe('generateTime()', () => {
minute: 43
}

const { hours, minutes, use24Hour } = generateTime('en-US', today, undefined, undefined, [1,2,3], [10,15,20]);
const { hours, minutes, use24Hour } = generateTime(today, 'h12', undefined, undefined, [1,2,3], [10,15,20]);

expect(hours).toStrictEqual([1,2,3]);
expect(minutes).toStrictEqual([10,15,20]);
Expand Down
6 changes: 6 additions & 0 deletions core/src/components/datetime/test/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ describe('isLeapYear()', () => {
describe('is24Hour()', () => {
it('should return true if the locale uses 24 hour time', () => {
expect(is24Hour('en-US')).toBe(false);
expect(is24Hour('en-US', 'h23')).toBe(true);
expect(is24Hour('en-US', 'h12')).toBe(false);
expect(is24Hour('en-US-u-hc-h23')).toBe(true);
expect(is24Hour('en-GB')).toBe(true);
expect(is24Hour('en-GB', 'h23')).toBe(true);
expect(is24Hour('en-GB', 'h12')).toBe(false);
expect(is24Hour('en-GB-u-hc-h12')).toBe(false);
})
})
15 changes: 15 additions & 0 deletions core/src/components/datetime/test/hour-cycle/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { newE2EPage } from '@stencil/core/testing';

test('hour-cycle', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/hour-cycle?ionic:_testing=true'
});

const screenshotCompares = [];

screenshotCompares.push(await page.compareScreenshot());

for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});
Loading