Skip to content

Commit 597bc3f

Browse files
authored
feat(datetime): add support for h11 and h24 hour formats (#28219)
Issue number: resolves #23750 --------- <!-- 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. --> Datetime does not support h11 and h24 hour formats ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Datetime supports h11 and h24 formats ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Implementation Notes: 1. I broke up the `is24Hour` function into two functions: - The first function, `is24Hour`, accepts an hour cycle and returns true if the hourCycle preference uses a 24 hour format - The second function, getHourCycle, accepts a locale and an optional hour cycle and returns the computed hour cycle. I found that the hour cycle is not always set via `hourCycle` (such as when we are using the system default if it's specified in the `locale` prop using locale extension tags). This was coupled to is24Hour, but I needed this functionality elsewhere to add support for this feature, so I decided to break the functions up. 2. We were using the hour cycle types in several places, so I decided to create a shared `DatetimeHourCycle` to avoid accidental typos.
1 parent d0d9e35 commit 597bc3f

File tree

13 files changed

+332
-72
lines changed

13 files changed

+332
-72
lines changed

core/api.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ ion-datetime,prop,disabled,boolean,false,false,false
394394
ion-datetime,prop,doneText,string,'Done',false,false
395395
ion-datetime,prop,firstDayOfWeek,number,0,false,false
396396
ion-datetime,prop,highlightedDates,((dateIsoString: string) => DatetimeHighlightStyle | undefined) | DatetimeHighlight[] | undefined,undefined,false,false
397-
ion-datetime,prop,hourCycle,"h12" | "h23" | undefined,undefined,false,false
397+
ion-datetime,prop,hourCycle,"h11" | "h12" | "h23" | "h24" | undefined,undefined,false,false
398398
ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false
399399
ion-datetime,prop,isDateEnabled,((dateIsoString: string) => boolean) | undefined,undefined,false,false
400400
ion-datetime,prop,locale,string,'default',false,false

core/src/components.d.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
1515
import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
1616
import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
1717
import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
18-
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
18+
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
1919
import { SpinnerTypes } from "./components/spinner/spinner-configs";
2020
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
2121
import { CounterFormatter } from "./components/item/item-interface";
@@ -51,7 +51,7 @@ export { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
5151
export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
5252
export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
5353
export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
54-
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
54+
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
5555
export { SpinnerTypes } from "./components/spinner/spinner-configs";
5656
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
5757
export { CounterFormatter } from "./components/item/item-interface";
@@ -865,7 +865,7 @@ export namespace Components {
865865
/**
866866
* The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale.
867867
*/
868-
"hourCycle"?: 'h23' | 'h12';
868+
"hourCycle"?: DatetimeHourCycle;
869869
/**
870870
* 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.
871871
*/
@@ -4889,7 +4889,7 @@ declare namespace LocalJSX {
48894889
/**
48904890
* The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale.
48914891
*/
4892-
"hourCycle"?: 'h23' | 'h12';
4892+
"hourCycle"?: DatetimeHourCycle;
48934893
/**
48944894
* 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.
48954895
*/

core/src/components/datetime-button/datetime-button.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { Color } from '../../interface';
99
import type { DatetimePresentation } from '../datetime/datetime-interface';
1010
import { getToday } from '../datetime/utils/data';
1111
import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
12-
import { is24Hour } from '../datetime/utils/helpers';
12+
import { getHourCycle } from '../datetime/utils/helpers';
1313
import { parseDate } from '../datetime/utils/parse';
1414
/**
1515
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@@ -218,15 +218,15 @@ export class DatetimeButton implements ComponentInterface {
218218
* warning in the console.
219219
*/
220220
const firstParsedDatetime = parsedDatetimes[0];
221-
const use24Hour = is24Hour(locale, hourCycle);
221+
const computedHourCycle = getHourCycle(locale, hourCycle);
222222

223223
this.dateText = this.timeText = undefined;
224224

225225
switch (datetimePresentation) {
226226
case 'date-time':
227227
case 'time-date':
228228
const dateText = getMonthDayAndYear(locale, firstParsedDatetime);
229-
const timeText = getLocalizedTime(locale, firstParsedDatetime, use24Hour);
229+
const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle);
230230
if (preferWheel) {
231231
this.dateText = `${dateText} ${timeText}`;
232232
} else {
@@ -250,7 +250,7 @@ export class DatetimeButton implements ComponentInterface {
250250
}
251251
break;
252252
case 'time':
253-
this.timeText = getLocalizedTime(locale, firstParsedDatetime, use24Hour);
253+
this.timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle);
254254
break;
255255
case 'month-year':
256256
this.dateText = getMonthAndYear(locale, firstParsedDatetime);

core/src/components/datetime/datetime-interface.ts

+2
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ export type DatetimeHighlightStyle =
3434
export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;
3535

3636
export type DatetimeHighlightCallback = (dateIsoString: string) => DatetimeHighlightStyle | undefined;
37+
38+
export type DatetimeHourCycle = 'h11' | 'h12' | 'h23' | 'h24';

core/src/components/datetime/datetime.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
DatetimeHighlight,
2020
DatetimeHighlightStyle,
2121
DatetimeHighlightCallback,
22+
DatetimeHourCycle,
2223
} from './datetime-interface';
2324
import { isSameDay, warnIfValueOutOfBounds, isBefore, isAfter } from './utils/comparison';
2425
import {
@@ -33,7 +34,7 @@ import {
3334
getCombinedDateColumnData,
3435
} from './utils/data';
3536
import { formatValue, getLocalizedTime, getMonthAndDay, getMonthAndYear } from './utils/format';
36-
import { is24Hour, isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth } from './utils/helpers';
37+
import { isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth, getHourCycle } from './utils/helpers';
3738
import {
3839
calculateHourFromAMPM,
3940
convertDataToISO,
@@ -422,7 +423,7 @@ export class Datetime implements ComponentInterface {
422423
* The hour cycle of the `ion-datetime`. If no value is set, this is
423424
* specified by the current locale.
424425
*/
425-
@Prop() hourCycle?: 'h23' | 'h12';
426+
@Prop() hourCycle?: DatetimeHourCycle;
426427

427428
/**
428429
* If `cover`, the `ion-datetime` will expand to cover the full width of its container.
@@ -2237,7 +2238,7 @@ export class Datetime implements ComponentInterface {
22372238

22382239
private renderTimeOverlay() {
22392240
const { hourCycle, isTimePopoverOpen, locale } = this;
2240-
const use24Hour = is24Hour(locale, hourCycle);
2241+
const computedHourCycle = getHourCycle(locale, hourCycle);
22412242
const activePart = this.getActivePartsWithFallback();
22422243

22432244
return [
@@ -2270,7 +2271,7 @@ export class Datetime implements ComponentInterface {
22702271
}
22712272
}}
22722273
>
2273-
{getLocalizedTime(locale, activePart, use24Hour)}
2274+
{getLocalizedTime(locale, activePart, computedHourCycle)}
22742275
</button>,
22752276
<ion-popover
22762277
alignment="center"

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

+137-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,125 @@
1-
import { generateMonths, getDaysOfWeek, generateTime, getToday, getCombinedDateColumnData } from '../utils/data';
1+
import {
2+
generateMonths,
3+
getDaysOfWeek,
4+
generateTime,
5+
getToday,
6+
getCombinedDateColumnData,
7+
getTimeColumnsData,
8+
} from '../utils/data';
9+
10+
// The minutes are the same across all hour cycles, so we don't check those
11+
describe('getTimeColumnsData()', () => {
12+
it('should generate formatted h12 hours and AM/PM data data', () => {
13+
const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
14+
const results = getTimeColumnsData('en-US', refParts, 'h12');
15+
16+
expect(results.hoursData).toEqual([
17+
{ text: '12', value: 0 },
18+
{ text: '1', value: 1 },
19+
{ text: '2', value: 2 },
20+
{ text: '3', value: 3 },
21+
{ text: '4', value: 4 },
22+
{ text: '5', value: 5 },
23+
{ text: '6', value: 6 },
24+
{ text: '7', value: 7 },
25+
{ text: '8', value: 8 },
26+
{ text: '9', value: 9 },
27+
{ text: '10', value: 10 },
28+
{ text: '11', value: 11 },
29+
]);
30+
expect(results.dayPeriodData).toEqual([
31+
{ text: 'AM', value: 'am' },
32+
{ text: 'PM', value: 'pm' },
33+
]);
34+
});
35+
it('should generate formatted h23 hours and AM/PM data data', () => {
36+
const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
37+
const results = getTimeColumnsData('en-US', refParts, 'h23');
38+
39+
expect(results.hoursData).toEqual([
40+
{ text: '00', value: 0 },
41+
{ text: '01', value: 1 },
42+
{ text: '02', value: 2 },
43+
{ text: '03', value: 3 },
44+
{ text: '04', value: 4 },
45+
{ text: '05', value: 5 },
46+
{ text: '06', value: 6 },
47+
{ text: '07', value: 7 },
48+
{ text: '08', value: 8 },
49+
{ text: '09', value: 9 },
50+
{ text: '10', value: 10 },
51+
{ text: '11', value: 11 },
52+
{ text: '12', value: 12 },
53+
{ text: '13', value: 13 },
54+
{ text: '14', value: 14 },
55+
{ text: '15', value: 15 },
56+
{ text: '16', value: 16 },
57+
{ text: '17', value: 17 },
58+
{ text: '18', value: 18 },
59+
{ text: '19', value: 19 },
60+
{ text: '20', value: 20 },
61+
{ text: '21', value: 21 },
62+
{ text: '22', value: 22 },
63+
{ text: '23', value: 23 },
64+
]);
65+
expect(results.dayPeriodData).toEqual([]);
66+
});
67+
it('should generate formatted h11 hours and AM/PM data data', () => {
68+
const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
69+
const results = getTimeColumnsData('en-US', refParts, 'h11');
70+
71+
expect(results.hoursData).toEqual([
72+
{ text: '0', value: 0 },
73+
{ text: '1', value: 1 },
74+
{ text: '2', value: 2 },
75+
{ text: '3', value: 3 },
76+
{ text: '4', value: 4 },
77+
{ text: '5', value: 5 },
78+
{ text: '6', value: 6 },
79+
{ text: '7', value: 7 },
80+
{ text: '8', value: 8 },
81+
{ text: '9', value: 9 },
82+
{ text: '10', value: 10 },
83+
{ text: '11', value: 11 },
84+
]);
85+
expect(results.dayPeriodData).toEqual([
86+
{ text: 'AM', value: 'am' },
87+
{ text: 'PM', value: 'pm' },
88+
]);
89+
});
90+
it('should generate formatted h24 hours and AM/PM data data', () => {
91+
const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
92+
const results = getTimeColumnsData('en-US', refParts, 'h24');
93+
94+
expect(results.hoursData).toEqual([
95+
{ text: '01', value: 1 },
96+
{ text: '02', value: 2 },
97+
{ text: '03', value: 3 },
98+
{ text: '04', value: 4 },
99+
{ text: '05', value: 5 },
100+
{ text: '06', value: 6 },
101+
{ text: '07', value: 7 },
102+
{ text: '08', value: 8 },
103+
{ text: '09', value: 9 },
104+
{ text: '10', value: 10 },
105+
{ text: '11', value: 11 },
106+
{ text: '12', value: 12 },
107+
{ text: '13', value: 13 },
108+
{ text: '14', value: 14 },
109+
{ text: '15', value: 15 },
110+
{ text: '16', value: 16 },
111+
{ text: '17', value: 17 },
112+
{ text: '18', value: 18 },
113+
{ text: '19', value: 19 },
114+
{ text: '20', value: 20 },
115+
{ text: '21', value: 21 },
116+
{ text: '22', value: 22 },
117+
{ text: '23', value: 23 },
118+
{ text: '24', value: 0 },
119+
]);
120+
expect(results.dayPeriodData).toEqual([]);
121+
});
122+
});
2123

3124
describe('generateMonths()', () => {
4125
it('should generate correct month data', () => {
@@ -41,7 +162,7 @@ describe('generateTime()', () => {
41162
hour: 5,
42163
minute: 43,
43164
};
44-
const { hours, minutes } = generateTime(today);
165+
const { hours, minutes } = generateTime('en-US', today);
45166

46167
expect(hours.length).toEqual(12);
47168
expect(minutes.length).toEqual(60);
@@ -61,7 +182,7 @@ describe('generateTime()', () => {
61182
hour: 2,
62183
minute: 40,
63184
};
64-
const { hours, minutes } = generateTime(today, 'h12', min);
185+
const { hours, minutes } = generateTime('en-US', today, 'h12', min);
65186

66187
expect(hours.length).toEqual(10);
67188
expect(minutes.length).toEqual(60);
@@ -81,7 +202,7 @@ describe('generateTime()', () => {
81202
hour: 2,
82203
minute: 40,
83204
};
84-
const { hours, minutes } = generateTime(today, 'h12', min);
205+
const { hours, minutes } = generateTime('en-US', today, 'h12', min);
85206

86207
expect(hours.length).toEqual(12);
87208
expect(minutes.length).toEqual(60);
@@ -101,7 +222,7 @@ describe('generateTime()', () => {
101222
hour: 7,
102223
minute: 44,
103224
};
104-
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
225+
const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
105226

106227
expect(hours.length).toEqual(8);
107228
expect(minutes.length).toEqual(45);
@@ -121,7 +242,7 @@ describe('generateTime()', () => {
121242
hour: 2,
122243
minute: 40,
123244
};
124-
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
245+
const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
125246

126247
expect(hours.length).toEqual(12);
127248
expect(minutes.length).toEqual(60);
@@ -141,7 +262,7 @@ describe('generateTime()', () => {
141262
hour: 2,
142263
minute: 40,
143264
};
144-
const { hours, minutes } = generateTime(today, 'h12', min);
265+
const { hours, minutes } = generateTime('en-US', today, 'h12', min);
145266

146267
expect(hours.length).toEqual(0);
147268
expect(minutes.length).toEqual(0);
@@ -161,7 +282,7 @@ describe('generateTime()', () => {
161282
hour: 2,
162283
minute: 40,
163284
};
164-
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
285+
const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
165286

166287
expect(hours.length).toEqual(0);
167288
expect(minutes.length).toEqual(0);
@@ -185,7 +306,7 @@ describe('generateTime()', () => {
185306
year: 2021,
186307
};
187308

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

190311
expect(hours.length).toEqual(12);
191312
expect(minutes.length).toEqual(60);
@@ -199,7 +320,7 @@ describe('generateTime()', () => {
199320
minute: 43,
200321
};
201322

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

204325
expect(hours).toStrictEqual([1, 2, 3]);
205326
expect(minutes).toStrictEqual([10, 15, 20]);
@@ -229,7 +350,7 @@ describe('generateTime()', () => {
229350
minute: 14,
230351
};
231352

232-
const { am, pm } = generateTime(today, 'h12', min, max);
353+
const { am, pm } = generateTime('en-US', today, 'h12', min, max);
233354

234355
expect(am).toBe(true);
235356
expect(pm).toBe(true);
@@ -253,7 +374,7 @@ describe('generateTime()', () => {
253374
minute: 50,
254375
};
255376

256-
const { hours } = generateTime(refValue, 'h23', minParts);
377+
const { hours } = generateTime('en-US', refValue, 'h23', minParts);
257378

258379
expect(hours).toStrictEqual([19, 20, 21, 22, 23]);
259380
});
@@ -276,7 +397,7 @@ describe('generateTime()', () => {
276397
minute: 30,
277398
};
278399

279-
const { hours, minutes } = generateTime(refValue, 'h23', minParts);
400+
const { hours, minutes } = generateTime('en-US', refValue, 'h23', minParts);
280401

281402
expect(hours).toStrictEqual([19, 20, 21, 22, 23]);
282403
expect(minutes.length).toEqual(60);
@@ -308,7 +429,7 @@ describe('generateTime()', () => {
308429
minute: 40,
309430
};
310431

311-
const { hours } = generateTime(refValue, 'h23', minParts, maxParts);
432+
const { hours } = generateTime('en-US', refValue, 'h23', minParts, maxParts);
312433

313434
expect(hours).toStrictEqual([19, 20]);
314435
});
@@ -330,7 +451,7 @@ describe('generateTime()', () => {
330451
minute: 2,
331452
};
332453

333-
const { minutes } = generateTime(refValue, 'h23', undefined, maxParts);
454+
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
334455

335456
expect(minutes).toStrictEqual([0, 1, 2]);
336457
});
@@ -352,7 +473,7 @@ describe('generateTime()', () => {
352473
minute: 2,
353474
};
354475

355-
const { minutes } = generateTime(refValue, 'h23', undefined, maxParts);
476+
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
356477

357478
expect(minutes.length).toEqual(60);
358479
});

0 commit comments

Comments
 (0)