Skip to content

Commit 6342fde

Browse files
feat(datetime): add hourCycle property (#23686)
resolves #23661 Co-authored-by: Liam DeBeasi <[email protected]>
1 parent ea39c70 commit 6342fde

File tree

17 files changed

+218
-32
lines changed

17 files changed

+218
-32
lines changed

angular/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ export class IonContent {
244244
}
245245
export declare interface IonDatetime extends Components.IonDatetime {
246246
}
247-
@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"] })
248-
@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"] })
247+
@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"] })
248+
@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"] })
249249
export class IonDatetime {
250250
ionCancel!: EventEmitter<CustomEvent>;
251251
ionChange!: EventEmitter<CustomEvent>;

core/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ ion-datetime,prop,color,string | undefined,'primary',false,false
374374
ion-datetime,prop,dayValues,number | number[] | string | undefined,undefined,false,false
375375
ion-datetime,prop,disabled,boolean,false,false,false
376376
ion-datetime,prop,doneText,string,'Done',false,false
377+
ion-datetime,prop,hourCycle,"h12" | "h23" | undefined,undefined,false,false
377378
ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false
378379
ion-datetime,prop,locale,string,'default',false,false
379380
ion-datetime,prop,max,string | undefined,undefined,false,false

core/src/components.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,10 @@ export namespace Components {
728728
* The text to display on the picker's "Done" button.
729729
*/
730730
"doneText": string;
731+
/**
732+
* The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale.
733+
*/
734+
"hourCycle"?: 'h23' | 'h12';
731735
/**
732736
* 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.
733737
*/
@@ -4292,6 +4296,10 @@ declare namespace LocalJSX {
42924296
* The text to display on the picker's "Done" button.
42934297
*/
42944298
"doneText"?: string;
4299+
/**
4300+
* The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale.
4301+
*/
4302+
"hourCycle"?: 'h23' | 'h12';
42954303
/**
42964304
* 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.
42974305
*/

core/src/components/datetime/datetime.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,12 @@ export class Datetime implements ComponentInterface {
318318
*/
319319
@Prop() showDefaultTimeLabel = true;
320320

321+
/**
322+
* The hour cycle of the `ion-datetime`. If no value is set, this is
323+
* specified by the current locale.
324+
*/
325+
@Prop() hourCycle?: 'h23' | 'h12';
326+
321327
/**
322328
* If `cover`, the `ion-datetime` will expand to cover the full width of its container.
323329
* If `fixed`, the `ion-datetime` will have a fixed width.
@@ -1397,9 +1403,10 @@ export class Datetime implements ComponentInterface {
13971403
* should just be the default segment.
13981404
*/
13991405
private renderTime(mode: Mode) {
1400-
const use24Hour = is24Hour(this.locale);
1406+
const { hourCycle } = this;
1407+
const use24Hour = is24Hour(this.locale, hourCycle);
14011408
const { ampm } = this.workingParts;
1402-
const { hours, minutes, am, pm } = generateTime(this.locale, this.workingParts, this.minParts, this.maxParts, this.parsedHourValues, this.parsedMinuteValues);
1409+
const { hours, minutes, am, pm } = generateTime(this.workingParts, use24Hour ? 'h23' : 'h12', this.minParts, this.maxParts, this.parsedHourValues, this.parsedMinuteValues);
14031410
return (
14041411
<div class="datetime-time">
14051412
<div class="time-header">

core/src/components/datetime/readme.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,39 @@ For instances where you need a specific locale, you can use the `locale` propert
7878
<ion-datetime locale="fr-FR"></ion-datetime>
7979
```
8080

81+
### Controlling the Hour Cycle
82+
83+
`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.
84+
85+
There are 4 primary hour cycle types:
86+
87+
| Hour cycle type | Description |
88+
| --------------- | ------------------------------------------------------------ |
89+
| `'h12` | Hour system using 1–12; corresponds to 'h' in patterns. The 12 hour clock, with midnight starting at 12:00 am. |
90+
| `'h23'` | Hour system using 0–23; corresponds to 'H' in patterns. The 24 hour clock, with midnight starting at 0:00. |
91+
| `'h11'` | Hour system using 0–11; corresponds to 'K' in patterns. The 12 hour clock, with midnight starting at 0:00 am. |
92+
| `'h24'` | Hour system using 1–24; corresponds to 'k' in pattern. The 24 hour clock, with midnight starting at 24:00. |
93+
94+
> Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
95+
96+
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.
97+
98+
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:
99+
100+
```html
101+
<ion-datetime hour-cycle="h12" locale="en-GB"></ion-datetime>
102+
```
103+
104+
`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.
105+
106+
For example, if you wanted to use a 12 hour cycle with the `en-GB` locale, you could alternatively do:
107+
108+
```html
109+
<ion-datetime locale="en-GB-u-hc-h12"></ion-datetime>
110+
```
111+
112+
`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)
113+
81114
## Parsing Dates
82115

83116
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.
@@ -176,6 +209,9 @@ dates in JavaScript.
176209
<!-- Full width size -->
177210
<ion-datetime size="cover"></ion-datetime>
178211

212+
<!-- Custom Hour Cycle -->
213+
<ion-datetime hour-cycle="h23"></ion-datetime>
214+
179215
<!-- Custom title -->
180216
<ion-datetime>
181217
<div slot="title">My Custom Title</div>
@@ -258,6 +294,9 @@ export class MyComponent {
258294
<!-- Full width size -->
259295
<ion-datetime size="cover"></ion-datetime>
260296

297+
<!-- Custom Hour Cycle -->
298+
<ion-datetime hour-cycle="h23"></ion-datetime>
299+
261300
<!-- Custom title -->
262301
<ion-datetime>
263302
<div slot="title">My Custom Title</div>
@@ -352,6 +391,9 @@ export const DateTimeExamples: React.FC = () => {
352391
{/* Full width size */}
353392
<IonDatetime size="cover"></IonDatetime>
354393

394+
{/* Custom Hour Cycle */}
395+
<IonDatetime hourCycle="h23"></IonDatetime>
396+
355397
{/* Custom title */}
356398
<IonDatetime>
357399
<div slot="title">My Custom Title</div>
@@ -436,6 +478,9 @@ export class DatetimeExample {
436478
{/* Full width size */}
437479
<ion-datetime size="cover"></ion-datetime>,
438480

481+
{/* Custom Hour Cycle */}
482+
<ion-datetime hourCycle="h23"></ion-datetime>,
483+
439484
{/* Custom title */}
440485
<ion-datetime>
441486
<div slot="title">My Custom Title</div>
@@ -496,6 +541,9 @@ export class DatetimeExample {
496541
<!-- Full width size -->
497542
<ion-datetime size="cover"></ion-datetime>
498543

544+
<!-- Custom Hour Cycle -->
545+
<ion-datetime hour-cycle="h23"></ion-datetime>
546+
499547
<!-- Custom title -->
500548
<ion-datetime>
501549
<div slot="title">My Custom Title</div>
@@ -569,6 +617,7 @@ export class DatetimeExample {
569617
| `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` |
570618
| `disabled` | `disabled` | If `true`, the user cannot interact with the datetime. | `boolean` | `false` |
571619
| `doneText` | `done-text` | The text to display on the picker's "Done" button. | `string` | `'Done'` |
620+
| `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` |
572621
| `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` |
573622
| `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'` |
574623
| `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` |

core/src/components/datetime/test/data.spec.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,10 @@ describe('generateTime()', () => {
4141
hour: 5,
4242
minute: 43
4343
}
44-
const { hours, minutes, use24Hour } = generateTime('en-US', today);
44+
const { hours, minutes } = generateTime(today);
4545

4646
expect(hours.length).toEqual(12);
4747
expect(minutes.length).toEqual(60);
48-
expect(use24Hour).toEqual(false);
4948
});
5049
it('should filter according to min', () => {
5150
const today = {
@@ -62,11 +61,10 @@ describe('generateTime()', () => {
6261
hour: 2,
6362
minute: 40
6463
}
65-
const { hours, minutes, use24Hour } = generateTime('en-US', today, min);
64+
const { hours, minutes } = generateTime(today, false, min);
6665

6766
expect(hours.length).toEqual(11);
6867
expect(minutes.length).toEqual(20);
69-
expect(use24Hour).toEqual(false);
7068
})
7169
it('should not filter according to min if not on reference day', () => {
7270
const today = {
@@ -83,11 +81,10 @@ describe('generateTime()', () => {
8381
hour: 2,
8482
minute: 40
8583
}
86-
const { hours, minutes, use24Hour } = generateTime('en-US', today, min);
84+
const { hours, minutes } = generateTime(today, 'h12', min);
8785

8886
expect(hours.length).toEqual(12);
8987
expect(minutes.length).toEqual(60);
90-
expect(use24Hour).toEqual(false);
9188
})
9289
it('should filter according to max', () => {
9390
const today = {
@@ -104,11 +101,10 @@ describe('generateTime()', () => {
104101
hour: 7,
105102
minute: 44
106103
}
107-
const { hours, minutes, use24Hour } = generateTime('en-US', today, undefined, max);
104+
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
108105

109106
expect(hours.length).toEqual(7);
110107
expect(minutes.length).toEqual(45);
111-
expect(use24Hour).toEqual(false);
112108
})
113109
it('should not filter according to min if not on reference day', () => {
114110
const today = {
@@ -125,11 +121,10 @@ describe('generateTime()', () => {
125121
hour: 2,
126122
minute: 40
127123
}
128-
const { hours, minutes, use24Hour } = generateTime('en-US', today, undefined, max);
124+
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
129125

130126
expect(hours.length).toEqual(12);
131127
expect(minutes.length).toEqual(60);
132-
expect(use24Hour).toEqual(false);
133128
})
134129
it('should return no values for a day less than the min', () => {
135130
const today = {
@@ -146,11 +141,10 @@ describe('generateTime()', () => {
146141
hour: 2,
147142
minute: 40
148143
}
149-
const { hours, minutes, use24Hour } = generateTime('en-US', today, min);
144+
const { hours, minutes } = generateTime(today, 'h12', min);
150145

151146
expect(hours.length).toEqual(0);
152147
expect(minutes.length).toEqual(0);
153-
expect(use24Hour).toEqual(false);
154148
})
155149
it('should return no values for a day greater than the max', () => {
156150
const today = {
@@ -167,11 +161,10 @@ describe('generateTime()', () => {
167161
hour: 2,
168162
minute: 40
169163
}
170-
const { hours, minutes, use24Hour } = generateTime('en-US', today, undefined, max);
164+
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
171165

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

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

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

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

212204
expect(hours).toStrictEqual([1,2,3]);
213205
expect(minutes).toStrictEqual([10,15,20]);

core/src/components/datetime/test/helpers.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ describe('isLeapYear()', () => {
4242
describe('is24Hour()', () => {
4343
it('should return true if the locale uses 24 hour time', () => {
4444
expect(is24Hour('en-US')).toBe(false);
45+
expect(is24Hour('en-US', 'h23')).toBe(true);
46+
expect(is24Hour('en-US', 'h12')).toBe(false);
47+
expect(is24Hour('en-US-u-hc-h23')).toBe(true);
4548
expect(is24Hour('en-GB')).toBe(true);
49+
expect(is24Hour('en-GB', 'h23')).toBe(true);
50+
expect(is24Hour('en-GB', 'h12')).toBe(false);
51+
expect(is24Hour('en-GB-u-hc-h12')).toBe(false);
4652
})
4753
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { newE2EPage } from '@stencil/core/testing';
2+
3+
test('hour-cycle', async () => {
4+
const page = await newE2EPage({
5+
url: '/src/components/datetime/test/hour-cycle?ionic:_testing=true'
6+
});
7+
8+
const screenshotCompares = [];
9+
10+
screenshotCompares.push(await page.compareScreenshot());
11+
12+
for (const screenshotCompare of screenshotCompares) {
13+
expect(screenshotCompare).toMatchScreenshot();
14+
}
15+
});

0 commit comments

Comments
 (0)