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.
This commit is contained in:
Liam DeBeasi
2023-09-28 11:40:46 -04:00
committed by GitHub
parent d0d9e35c37
commit 597bc3f085
13 changed files with 333 additions and 73 deletions

View File

@ -394,7 +394,7 @@ ion-datetime,prop,disabled,boolean,false,false,false
ion-datetime,prop,doneText,string,'Done',false,false
ion-datetime,prop,firstDayOfWeek,number,0,false,false
ion-datetime,prop,highlightedDates,((dateIsoString: string) => DatetimeHighlightStyle | undefined) | DatetimeHighlight[] | undefined,undefined,false,false
ion-datetime,prop,hourCycle,"h12" | "h23" | undefined,undefined,false,false
ion-datetime,prop,hourCycle,"h11" | "h12" | "h23" | "h24" | undefined,undefined,false,false
ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,prop,isDateEnabled,((dateIsoString: string) => boolean) | undefined,undefined,false,false
ion-datetime,prop,locale,string,'default',false,false

View File

@ -15,7 +15,7 @@ import { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
import { SpinnerTypes } from "./components/spinner/spinner-configs";
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
import { CounterFormatter } from "./components/item/item-interface";
@ -51,7 +51,7 @@ export { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
export { SpinnerTypes } from "./components/spinner/spinner-configs";
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
export { CounterFormatter } from "./components/item/item-interface";
@ -865,7 +865,7 @@ export namespace Components {
/**
* The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale.
*/
"hourCycle"?: 'h23' | 'h12';
"hourCycle"?: DatetimeHourCycle;
/**
* 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.
*/
@ -4889,7 +4889,7 @@ declare namespace LocalJSX {
/**
* The hour cycle of the `ion-datetime`. If no value is set, this is specified by the current locale.
*/
"hourCycle"?: 'h23' | 'h12';
"hourCycle"?: DatetimeHourCycle;
/**
* 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.
*/

View File

@ -9,7 +9,7 @@ import type { Color } from '../../interface';
import type { DatetimePresentation } from '../datetime/datetime-interface';
import { getToday } from '../datetime/utils/data';
import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
import { is24Hour } from '../datetime/utils/helpers';
import { getHourCycle } from '../datetime/utils/helpers';
import { parseDate } from '../datetime/utils/parse';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@ -218,7 +218,7 @@ export class DatetimeButton implements ComponentInterface {
* warning in the console.
*/
const firstParsedDatetime = parsedDatetimes[0];
const use24Hour = is24Hour(locale, hourCycle);
const computedHourCycle = getHourCycle(locale, hourCycle);
this.dateText = this.timeText = undefined;
@ -226,7 +226,7 @@ export class DatetimeButton implements ComponentInterface {
case 'date-time':
case 'time-date':
const dateText = getMonthDayAndYear(locale, firstParsedDatetime);
const timeText = getLocalizedTime(locale, firstParsedDatetime, use24Hour);
const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle);
if (preferWheel) {
this.dateText = `${dateText} ${timeText}`;
} else {
@ -250,7 +250,7 @@ export class DatetimeButton implements ComponentInterface {
}
break;
case 'time':
this.timeText = getLocalizedTime(locale, firstParsedDatetime, use24Hour);
this.timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle);
break;
case 'month-year':
this.dateText = getMonthAndYear(locale, firstParsedDatetime);

View File

@ -34,3 +34,5 @@ export type DatetimeHighlightStyle =
export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;
export type DatetimeHighlightCallback = (dateIsoString: string) => DatetimeHighlightStyle | undefined;
export type DatetimeHourCycle = 'h11' | 'h12' | 'h23' | 'h24';

View File

@ -19,6 +19,7 @@ import type {
DatetimeHighlight,
DatetimeHighlightStyle,
DatetimeHighlightCallback,
DatetimeHourCycle,
} from './datetime-interface';
import { isSameDay, warnIfValueOutOfBounds, isBefore, isAfter } from './utils/comparison';
import {
@ -33,7 +34,7 @@ import {
getCombinedDateColumnData,
} from './utils/data';
import { formatValue, getLocalizedTime, getMonthAndDay, getMonthAndYear } from './utils/format';
import { is24Hour, isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth } from './utils/helpers';
import { isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth, getHourCycle } from './utils/helpers';
import {
calculateHourFromAMPM,
convertDataToISO,
@ -422,7 +423,7 @@ export class Datetime implements ComponentInterface {
* The hour cycle of the `ion-datetime`. If no value is set, this is
* specified by the current locale.
*/
@Prop() hourCycle?: 'h23' | 'h12';
@Prop() hourCycle?: DatetimeHourCycle;
/**
* If `cover`, the `ion-datetime` will expand to cover the full width of its container.
@ -2237,7 +2238,7 @@ export class Datetime implements ComponentInterface {
private renderTimeOverlay() {
const { hourCycle, isTimePopoverOpen, locale } = this;
const use24Hour = is24Hour(locale, hourCycle);
const computedHourCycle = getHourCycle(locale, hourCycle);
const activePart = this.getActivePartsWithFallback();
return [
@ -2270,7 +2271,7 @@ export class Datetime implements ComponentInterface {
}
}}
>
{getLocalizedTime(locale, activePart, use24Hour)}
{getLocalizedTime(locale, activePart, computedHourCycle)}
</button>,
<ion-popover
alignment="center"

View File

@ -1,4 +1,125 @@
import { generateMonths, getDaysOfWeek, generateTime, getToday, getCombinedDateColumnData } from '../utils/data';
import {
generateMonths,
getDaysOfWeek,
generateTime,
getToday,
getCombinedDateColumnData,
getTimeColumnsData,
} from '../utils/data';
// The minutes are the same across all hour cycles, so we don't check those
describe('getTimeColumnsData()', () => {
it('should generate formatted h12 hours and AM/PM data data', () => {
const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
const results = getTimeColumnsData('en-US', refParts, 'h12');
expect(results.hoursData).toEqual([
{ text: '12', value: 0 },
{ text: '1', value: 1 },
{ text: '2', value: 2 },
{ text: '3', value: 3 },
{ text: '4', value: 4 },
{ text: '5', value: 5 },
{ text: '6', value: 6 },
{ text: '7', value: 7 },
{ text: '8', value: 8 },
{ text: '9', value: 9 },
{ text: '10', value: 10 },
{ text: '11', value: 11 },
]);
expect(results.dayPeriodData).toEqual([
{ text: 'AM', value: 'am' },
{ text: 'PM', value: 'pm' },
]);
});
it('should generate formatted h23 hours and AM/PM data data', () => {
const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
const results = getTimeColumnsData('en-US', refParts, 'h23');
expect(results.hoursData).toEqual([
{ text: '00', value: 0 },
{ text: '01', value: 1 },
{ text: '02', value: 2 },
{ text: '03', value: 3 },
{ text: '04', value: 4 },
{ text: '05', value: 5 },
{ text: '06', value: 6 },
{ text: '07', value: 7 },
{ text: '08', value: 8 },
{ text: '09', value: 9 },
{ text: '10', value: 10 },
{ text: '11', value: 11 },
{ text: '12', value: 12 },
{ text: '13', value: 13 },
{ text: '14', value: 14 },
{ text: '15', value: 15 },
{ text: '16', value: 16 },
{ text: '17', value: 17 },
{ text: '18', value: 18 },
{ text: '19', value: 19 },
{ text: '20', value: 20 },
{ text: '21', value: 21 },
{ text: '22', value: 22 },
{ text: '23', value: 23 },
]);
expect(results.dayPeriodData).toEqual([]);
});
it('should generate formatted h11 hours and AM/PM data data', () => {
const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
const results = getTimeColumnsData('en-US', refParts, 'h11');
expect(results.hoursData).toEqual([
{ text: '0', value: 0 },
{ text: '1', value: 1 },
{ text: '2', value: 2 },
{ text: '3', value: 3 },
{ text: '4', value: 4 },
{ text: '5', value: 5 },
{ text: '6', value: 6 },
{ text: '7', value: 7 },
{ text: '8', value: 8 },
{ text: '9', value: 9 },
{ text: '10', value: 10 },
{ text: '11', value: 11 },
]);
expect(results.dayPeriodData).toEqual([
{ text: 'AM', value: 'am' },
{ text: 'PM', value: 'pm' },
]);
});
it('should generate formatted h24 hours and AM/PM data data', () => {
const refParts = { month: 5, year: 2021, day: 1, hour: 4, minute: 30 };
const results = getTimeColumnsData('en-US', refParts, 'h24');
expect(results.hoursData).toEqual([
{ text: '01', value: 1 },
{ text: '02', value: 2 },
{ text: '03', value: 3 },
{ text: '04', value: 4 },
{ text: '05', value: 5 },
{ text: '06', value: 6 },
{ text: '07', value: 7 },
{ text: '08', value: 8 },
{ text: '09', value: 9 },
{ text: '10', value: 10 },
{ text: '11', value: 11 },
{ text: '12', value: 12 },
{ text: '13', value: 13 },
{ text: '14', value: 14 },
{ text: '15', value: 15 },
{ text: '16', value: 16 },
{ text: '17', value: 17 },
{ text: '18', value: 18 },
{ text: '19', value: 19 },
{ text: '20', value: 20 },
{ text: '21', value: 21 },
{ text: '22', value: 22 },
{ text: '23', value: 23 },
{ text: '24', value: 0 },
]);
expect(results.dayPeriodData).toEqual([]);
});
});
describe('generateMonths()', () => {
it('should generate correct month data', () => {
@ -41,7 +162,7 @@ describe('generateTime()', () => {
hour: 5,
minute: 43,
};
const { hours, minutes } = generateTime(today);
const { hours, minutes } = generateTime('en-US', today);
expect(hours.length).toEqual(12);
expect(minutes.length).toEqual(60);
@ -61,7 +182,7 @@ describe('generateTime()', () => {
hour: 2,
minute: 40,
};
const { hours, minutes } = generateTime(today, 'h12', min);
const { hours, minutes } = generateTime('en-US', today, 'h12', min);
expect(hours.length).toEqual(10);
expect(minutes.length).toEqual(60);
@ -81,7 +202,7 @@ describe('generateTime()', () => {
hour: 2,
minute: 40,
};
const { hours, minutes } = generateTime(today, 'h12', min);
const { hours, minutes } = generateTime('en-US', today, 'h12', min);
expect(hours.length).toEqual(12);
expect(minutes.length).toEqual(60);
@ -101,7 +222,7 @@ describe('generateTime()', () => {
hour: 7,
minute: 44,
};
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
expect(hours.length).toEqual(8);
expect(minutes.length).toEqual(45);
@ -121,7 +242,7 @@ describe('generateTime()', () => {
hour: 2,
minute: 40,
};
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
expect(hours.length).toEqual(12);
expect(minutes.length).toEqual(60);
@ -141,7 +262,7 @@ describe('generateTime()', () => {
hour: 2,
minute: 40,
};
const { hours, minutes } = generateTime(today, 'h12', min);
const { hours, minutes } = generateTime('en-US', today, 'h12', min);
expect(hours.length).toEqual(0);
expect(minutes.length).toEqual(0);
@ -161,7 +282,7 @@ describe('generateTime()', () => {
hour: 2,
minute: 40,
};
const { hours, minutes } = generateTime(today, 'h12', undefined, max);
const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, max);
expect(hours.length).toEqual(0);
expect(minutes.length).toEqual(0);
@ -185,7 +306,7 @@ describe('generateTime()', () => {
year: 2021,
};
const { hours, minutes } = generateTime(today, 'h12', min, max);
const { hours, minutes } = generateTime('en-US', today, 'h12', min, max);
expect(hours.length).toEqual(12);
expect(minutes.length).toEqual(60);
@ -199,7 +320,7 @@ describe('generateTime()', () => {
minute: 43,
};
const { hours, minutes } = generateTime(today, 'h12', undefined, undefined, [1, 2, 3], [10, 15, 20]);
const { hours, minutes } = generateTime('en-US', today, 'h12', undefined, undefined, [1, 2, 3], [10, 15, 20]);
expect(hours).toStrictEqual([1, 2, 3]);
expect(minutes).toStrictEqual([10, 15, 20]);
@ -229,7 +350,7 @@ describe('generateTime()', () => {
minute: 14,
};
const { am, pm } = generateTime(today, 'h12', min, max);
const { am, pm } = generateTime('en-US', today, 'h12', min, max);
expect(am).toBe(true);
expect(pm).toBe(true);
@ -253,7 +374,7 @@ describe('generateTime()', () => {
minute: 50,
};
const { hours } = generateTime(refValue, 'h23', minParts);
const { hours } = generateTime('en-US', refValue, 'h23', minParts);
expect(hours).toStrictEqual([19, 20, 21, 22, 23]);
});
@ -276,7 +397,7 @@ describe('generateTime()', () => {
minute: 30,
};
const { hours, minutes } = generateTime(refValue, 'h23', minParts);
const { hours, minutes } = generateTime('en-US', refValue, 'h23', minParts);
expect(hours).toStrictEqual([19, 20, 21, 22, 23]);
expect(minutes.length).toEqual(60);
@ -308,7 +429,7 @@ describe('generateTime()', () => {
minute: 40,
};
const { hours } = generateTime(refValue, 'h23', minParts, maxParts);
const { hours } = generateTime('en-US', refValue, 'h23', minParts, maxParts);
expect(hours).toStrictEqual([19, 20]);
});
@ -330,7 +451,7 @@ describe('generateTime()', () => {
minute: 2,
};
const { minutes } = generateTime(refValue, 'h23', undefined, maxParts);
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
expect(minutes).toStrictEqual([0, 1, 2]);
});
@ -352,7 +473,7 @@ describe('generateTime()', () => {
minute: 2,
};
const { minutes } = generateTime(refValue, 'h23', undefined, maxParts);
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
expect(minutes.length).toEqual(60);
});

View File

@ -56,11 +56,17 @@ describe('getMonthAndDay()', () => {
describe('getFormattedHour()', () => {
it('should only add padding if using 24 hour time', () => {
expect(getFormattedHour(0, true)).toEqual('00');
expect(getFormattedHour(0, false)).toEqual('12');
expect(getFormattedHour(1, 'h11')).toEqual('1');
expect(getFormattedHour(1, 'h12')).toEqual('1');
expect(getFormattedHour(1, 'h23')).toEqual('01');
expect(getFormattedHour(1, 'h24')).toEqual('01');
});
expect(getFormattedHour(10, true)).toEqual('10');
expect(getFormattedHour(10, false)).toEqual('10');
it('should return correct hour value for hour cycle', () => {
expect(getFormattedHour(0, 'h11')).toEqual('0');
expect(getFormattedHour(0, 'h12')).toEqual('12');
expect(getFormattedHour(0, 'h23')).toEqual('00');
expect(getFormattedHour(0, 'h24')).toEqual('24');
});
});
@ -111,7 +117,7 @@ describe('getLocalizedTime', () => {
minute: 40,
};
expect(getLocalizedTime('en-US', datetimeParts, false)).toEqual('1:40 PM');
expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('1:40 PM');
});
it('should localize the time to AM', () => {
@ -123,7 +129,7 @@ describe('getLocalizedTime', () => {
minute: 40,
};
expect(getLocalizedTime('en-US', datetimeParts, false)).toEqual('9:40 AM');
expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('9:40 AM');
});
it('should avoid Chromium bug when using 12 hour time in a 24 hour locale', () => {
@ -135,7 +141,7 @@ describe('getLocalizedTime', () => {
minute: 0,
};
expect(getLocalizedTime('en-GB', datetimeParts, false)).toEqual('12:00 am');
expect(getLocalizedTime('en-GB', datetimeParts, 'h12')).toEqual('12:00 am');
});
it('should parse time-only values correctly', () => {
const datetimeParts = {
@ -143,7 +149,7 @@ describe('getLocalizedTime', () => {
minute: 40,
};
expect(getLocalizedTime('en-US', datetimeParts, false)).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts, true)).toEqual('22:40');
expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts, 'h23')).toEqual('22:40');
});
});

View File

@ -1,4 +1,4 @@
import { isLeapYear, getNumDaysInMonth, is24Hour, isMonthFirstLocale } from '../utils/helpers';
import { isLeapYear, getNumDaysInMonth, is24Hour, isMonthFirstLocale, getHourCycle } from '../utils/helpers';
describe('daysInMonth()', () => {
it('should return correct days in month for month and year', () => {
@ -37,14 +37,29 @@ 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);
expect(is24Hour('h11')).toBe(false);
expect(is24Hour('h12')).toBe(false);
expect(is24Hour('h23')).toBe(true);
expect(is24Hour('h24')).toBe(true);
});
});
describe('getHourCycle()', () => {
it('should return the correct hour cycle', () => {
expect(getHourCycle('en-US')).toBe('h12');
expect(getHourCycle('en-US', 'h23')).toBe('h23');
expect(getHourCycle('en-US', 'h12')).toBe('h12');
expect(getHourCycle('en-US-u-hc-h23')).toBe('h23');
expect(getHourCycle('en-GB')).toBe('h23');
expect(getHourCycle('en-GB', 'h23')).toBe('h23');
expect(getHourCycle('en-GB', 'h12')).toBe('h12');
expect(getHourCycle('en-GB-u-hc-h12')).toBe('h12');
expect(getHourCycle('en-GB', 'h11')).toBe('h11');
expect(getHourCycle('en-GB-u-hc-h11')).toBe('h11');
expect(getHourCycle('en-GB', 'h24')).toBe('h24');
expect(getHourCycle('en-GB-u-hc-h24')).toBe('h24');
});
});

View File

@ -25,5 +25,27 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const timeButton = page.locator('ion-datetime .time-body');
await expect(timeButton).toHaveText('4:30 PM');
});
test('should set the h11 hour cycle correctly', async ({ page }) => {
await page.setContent(
`
<ion-datetime hour-cycle="h11" value="2022-01-01T00:30:00"></ion-datetime>
`,
config
);
const timeButton = page.locator('ion-datetime .time-body');
await expect(timeButton).toHaveText('0:30 AM');
});
test('should set the h24 hour cycle correctly', async ({ page }) => {
await page.setContent(
`
<ion-datetime hour-cycle="h24" value="2022-01-01T00:30:00"></ion-datetime>
`,
config
);
const timeButton = page.locator('ion-datetime .time-body');
await expect(timeButton).toHaveText('24:30');
});
});
});

View File

@ -51,6 +51,14 @@
<h2>h12 Hour cycle</h2>
<ion-datetime hour-cycle="h12"></ion-datetime>
</div>
<div class="grid-item">
<h2>h11 Hour cycle</h2>
<ion-datetime hour-cycle="h11"></ion-datetime>
</div>
<div class="grid-item">
<h2>h24 Hour cycle</h2>
<ion-datetime hour-cycle="h24"></ion-datetime>
</div>
<div class="grid-item">
<h2>h23 Hour cycle (Extension Tag)</h2>
<ion-datetime locale="en-US-u-hc-h23"></ion-datetime>

View File

@ -1,6 +1,6 @@
import type { Mode } from '../../../interface';
import type { PickerColumnItem } from '../../picker-column-internal/picker-column-internal-interfaces';
import type { DatetimeParts } from '../datetime-interface';
import type { DatetimeParts, DatetimeHourCycle } from '../datetime-interface';
import { isAfter, isBefore, isSameDay } from './comparison';
import {
@ -11,7 +11,7 @@ import {
getTodayLabel,
getYear,
} from './format';
import { getNumDaysInMonth, is24Hour } from './helpers';
import { getNumDaysInMonth, is24Hour, getHourCycle } from './helpers';
import { getNextMonth, getPreviousMonth, getInternalHourValue } from './manipulation';
/**
@ -44,9 +44,19 @@ const minutes = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
];
// h11 hour system uses 0-11. Midnight starts at 0:00am.
const hour11 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
// h12 hour system uses 0-12. Midnight starts at 12:00am.
const hour12 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
// h23 hour system uses 0-23. Midnight starts at 0:00.
const hour23 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
// h24 hour system uses 1-24. Midnight starts at 24:00.
const hour24 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 0];
/**
* Given a locale and a mode,
* return an array with formatted days
@ -125,21 +135,42 @@ export const getDaysOfMonth = (month: number, year: number, firstDayOfWeek: numb
return days;
};
/**
* Returns an array of pre-defined hour
* values based on the provided hourCycle.
*/
const getHourData = (hourCycle: DatetimeHourCycle) => {
switch (hourCycle) {
case 'h11':
return hour11;
case 'h12':
return hour12;
case 'h23':
return hour23;
case 'h24':
return hour24;
default:
throw new Error(`Invalid hour cycle "${hourCycle}"`);
}
};
/**
* Given a local, reference datetime parts and option
* max/min bound datetime parts, calculate the acceptable
* hour and minute values according to the bounds and locale.
*/
export const generateTime = (
locale: string,
refParts: DatetimeParts,
hourCycle: 'h12' | 'h23' = 'h12',
hourCycle: DatetimeHourCycle = 'h12',
minParts?: DatetimeParts,
maxParts?: DatetimeParts,
hourValues?: number[],
minuteValues?: number[]
) => {
const use24Hour = hourCycle === 'h23';
let processedHours = use24Hour ? hour23 : hour12;
const computedHourCycle = getHourCycle(locale, hourCycle);
const use24Hour = is24Hour(computedHourCycle);
let processedHours = getHourData(computedHourCycle);
let processedMinutes = minutes;
let isAMAllowed = true;
let isPMAllowed = true;
@ -540,16 +571,18 @@ export const getCombinedDateColumnData = (
export const getTimeColumnsData = (
locale: string,
refParts: DatetimeParts,
hourCycle?: 'h23' | 'h12',
hourCycle?: DatetimeHourCycle,
minParts?: DatetimeParts,
maxParts?: DatetimeParts,
allowedHourValues?: number[],
allowedMinuteValues?: number[]
): { [key: string]: PickerColumnItem[] } => {
const use24Hour = is24Hour(locale, hourCycle);
const computedHourCycle = getHourCycle(locale, hourCycle);
const use24Hour = is24Hour(computedHourCycle);
const { hours, minutes, am, pm } = generateTime(
locale,
refParts,
use24Hour ? 'h23' : 'h12',
computedHourCycle,
minParts,
maxParts,
allowedHourValues,
@ -558,7 +591,7 @@ export const getTimeColumnsData = (
const hoursItems = hours.map((hour) => {
return {
text: getFormattedHour(hour, use24Hour),
text: getFormattedHour(hour, computedHourCycle),
value: getInternalHourValue(hour, use24Hour, refParts.ampm),
};
});

View File

@ -1,5 +1,6 @@
import type { DatetimeParts } from '../datetime-interface';
import type { DatetimeParts, DatetimeHourCycle } from '../datetime-interface';
import { is24Hour } from './helpers';
import { convertDataToISO } from './manipulation';
const getFormattedDayPeriod = (dayPeriod?: string) => {
@ -10,7 +11,7 @@ const getFormattedDayPeriod = (dayPeriod?: string) => {
return dayPeriod.toUpperCase();
};
export const getLocalizedTime = (locale: string, refParts: DatetimeParts, use24Hour: boolean): string => {
export const getLocalizedTime = (locale: string, refParts: DatetimeParts, hourCycle: DatetimeHourCycle): string => {
const timeParts: Pick<DatetimeParts, 'hour' | 'minute'> = {
hour: refParts.hour,
minute: refParts.minute,
@ -34,7 +35,7 @@ export const getLocalizedTime = (locale: string, refParts: DatetimeParts, use24H
* We use hourCycle here instead of hour12 due to:
* https://bugs.chromium.org/p/chromium/issues/detail?id=1347316&q=hour12&can=2
*/
hourCycle: use24Hour ? 'h23' : 'h12',
hourCycle,
/**
* Setting Z at the end indicates that this
* date string is in the UTC time zone. This
@ -83,18 +84,34 @@ export const addTimePadding = (value: number): string => {
* 12 hour times it ensures that
* hour 0 is formatted as '12'.
*/
export const getFormattedHour = (hour: number, use24Hour: boolean): string => {
if (use24Hour) {
return addTimePadding(hour);
}
export const getFormattedHour = (hour: number, hourCycle: DatetimeHourCycle): string => {
/**
* If using 12 hour
* format, make sure hour
* 0 is formatted as '12'.
* Midnight for h11 starts at 0:00am
* Midnight for h12 starts at 12:00am
* Midnight for h23 starts at 00:00
* Midnight for h24 starts at 24:00
*/
if (hour === 0) {
switch (hourCycle) {
case 'h11':
return '0';
case 'h12':
return '12';
case 'h23':
return '00';
case 'h24':
return '24';
default:
throw new Error(`Invalid hour cycle "${hourCycle}"`);
}
}
const use24Hour = is24Hour(hourCycle);
/**
* h23 and h24 use 24 hour times.
*/
if (use24Hour) {
return addTimePadding(hour);
}
return hour.toString();

View File

@ -1,3 +1,5 @@
import type { DatetimeHourCycle } from '../datetime-interface';
/**
* Determines if given year is a
* leap year. Returns `true` if year
@ -8,13 +10,19 @@ export const isLeapYear = (year: number) => {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
};
export const is24Hour = (locale: string, hourCycle?: 'h23' | 'h12') => {
/**
* If developer has explicitly enabled h23 time
* Determines the hour cycle for a user.
* If the hour cycle is explicitly defined, just use that.
* Otherwise, we try to derive it from either the specified
* locale extension tags or from Intl.DateTimeFormat directly.
*/
export const getHourCycle = (locale: string, hourCycle?: DatetimeHourCycle) => {
/**
* If developer has explicitly enabled 24-hour time
* then return early and do not look at the system default.
*/
if (hourCycle !== undefined) {
return hourCycle === 'h23';
return hourCycle;
}
/**
@ -26,7 +34,7 @@ export const is24Hour = (locale: string, hourCycle?: 'h23' | 'h12') => {
const formatted = new Intl.DateTimeFormat(locale, { hour: 'numeric' });
const options = formatted.resolvedOptions();
if (options.hourCycle !== undefined) {
return options.hourCycle === 'h23';
return options.hourCycle;
}
/**
@ -42,7 +50,34 @@ export const is24Hour = (locale: string, hourCycle?: 'h23' | 'h12') => {
throw new Error('Hour value not found from DateTimeFormat');
}
return hour.value === '00';
/**
* Midnight for h11 starts at 0:00am
* Midnight for h12 starts at 12:00am
* Midnight for h23 starts at 00:00
* Midnight for h24 starts at 24:00
*/
switch (hour.value) {
case '0':
return 'h11';
case '12':
return 'h12';
case '00':
return 'h23';
case '24':
return 'h24';
default:
throw new Error(`Invalid hour cycle "${hourCycle}"`);
}
};
/**
* Determine if the hour cycle uses a 24-hour format.
* Returns true for h23 and h24. Returns false otherwise.
* If you don't know the hourCycle, use getHourCycle above
* and pass the result into this function.
*/
export const is24Hour = (hourCycle: DatetimeHourCycle) => {
return hourCycle === 'h23' || hourCycle === 'h24';
};
/**