mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-08 15:51:16 +08:00
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:
@ -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
|
||||
|
||||
8
core/src/components.d.ts
vendored
8
core/src/components.d.ts
vendored
@ -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.
|
||||
*/
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
return '12';
|
||||
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();
|
||||
|
||||
@ -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') => {
|
||||
/**
|
||||
* 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 h23 time
|
||||
* 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';
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user