chore(): sync with main

This commit is contained in:
Liam DeBeasi
2022-09-16 15:24:24 -04:00
759 changed files with 13280 additions and 7837 deletions

View File

@@ -47,7 +47,15 @@ import {
getPreviousYear,
getStartOfWeek,
} from './utils/manipulation';
import { clampDate, convertToArrayOfNumbers, getPartsFromCalendarDay, parseAmPm, parseDate } from './utils/parse';
import {
clampDate,
convertToArrayOfNumbers,
getPartsFromCalendarDay,
parseAmPm,
parseDate,
parseMaxParts,
parseMinParts,
} from './utils/parse';
import {
getCalendarDayState,
isDayDisabled,
@@ -637,18 +645,8 @@ export class Datetime implements ComponentInterface {
return presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
}
/**
* Stencil sometimes sets calendarBodyRef to null on rerender, even though
* the element is present. Query for it manually as a fallback.
*
* TODO(FW-901) Remove when issue is resolved: https://github.com/ionic-team/stencil/issues/3253
*/
private getCalendarBodyEl = () => {
return this.calendarBodyRef || this.el.shadowRoot?.querySelector('.calendar-body');
};
private initializeKeyboardListeners = () => {
const calendarBodyRef = this.getCalendarBodyEl();
const calendarBodyRef = this.calendarBodyRef;
if (!calendarBodyRef) {
return;
}
@@ -792,41 +790,28 @@ export class Datetime implements ComponentInterface {
};
private processMinParts = () => {
if (this.min === undefined) {
const { min, todayParts } = this;
if (min === undefined) {
this.minParts = undefined;
return;
}
const { month, day, year, hour, minute } = parseDate(this.min);
this.minParts = {
month,
day,
year,
hour,
minute,
};
this.minParts = parseMinParts(min, todayParts);
};
private processMaxParts = () => {
if (this.max === undefined) {
const { max, todayParts } = this;
if (max === undefined) {
this.maxParts = undefined;
return;
}
const { month, day, year, hour, minute } = parseDate(this.max);
this.maxParts = {
month,
day,
year,
hour,
minute,
};
this.maxParts = parseMaxParts(max, todayParts);
};
private initializeCalendarListener = () => {
const calendarBodyRef = this.getCalendarBodyEl();
const calendarBodyRef = this.calendarBodyRef;
if (!calendarBodyRef) {
return;
}
@@ -1129,7 +1114,28 @@ export class Datetime implements ComponentInterface {
* so we need to re-init behavior with the new elements.
*/
componentDidRender() {
const { presentation, prevPresentation } = this;
const { presentation, prevPresentation, calendarBodyRef, minParts, preferWheel } = this;
/**
* TODO(FW-2165)
* Remove this when https://bugs.webkit.org/show_bug.cgi?id=235960 is fixed.
* When using `min`, we add `scroll-snap-align: none`
* to the disabled month so that users cannot scroll to it.
* This triggers a bug in WebKit where the scroll position is reset.
* Since the month change logic is handled by a scroll listener,
* this causes the month to change leading to `scroll-snap-align`
* changing again, thus changing the scroll position again and causing
* an infinite loop.
* This issue only applies to the calendar grid, so we can disable
* it if the calendar grid is not being used.
*/
const hasCalendarGrid = !preferWheel && ['date-time', 'time-date', 'date'].includes(presentation);
if (minParts !== undefined && hasCalendarGrid && calendarBodyRef) {
const workingMonth = calendarBodyRef.querySelector('.calendar-month:nth-of-type(1)');
if (workingMonth) {
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
}
}
if (prevPresentation === null) {
this.prevPresentation = presentation;
@@ -1158,7 +1164,7 @@ export class Datetime implements ComponentInterface {
}
private processValue = (value?: string | string[] | null) => {
this.highlightActiveParts = value !== null && value !== undefined;
const hasValue = (this.highlightActiveParts = value !== null && value !== undefined);
let valueToProcess = parseDate(value ?? getToday());
const { minParts, maxParts, multiple } = this;
@@ -1167,7 +1173,17 @@ export class Datetime implements ComponentInterface {
valueToProcess = (valueToProcess as DatetimeParts[])[0];
}
warnIfValueOutOfBounds(valueToProcess, minParts, maxParts);
/**
* Datetime should only warn of out of bounds values
* if set by the user. If the `value` is undefined,
* we will default to today's date which may be out
* of bounds. In this case, the warning makes it look
* like the developer did something wrong which is
* not true.
*/
if (hasValue) {
warnIfValueOutOfBounds(valueToProcess, minParts, maxParts);
}
/**
* If there are multiple values, pick an arbitrary one to clamp to. This way,
@@ -1249,7 +1265,7 @@ export class Datetime implements ComponentInterface {
};
private nextMonth = () => {
const calendarBodyRef = this.getCalendarBodyEl();
const calendarBodyRef = this.calendarBodyRef;
if (!calendarBodyRef) {
return;
}
@@ -1269,7 +1285,7 @@ export class Datetime implements ComponentInterface {
};
private prevMonth = () => {
const calendarBodyRef = this.getCalendarBodyEl();
const calendarBodyRef = this.calendarBodyRef;
if (!calendarBodyRef) {
return;
}
@@ -1406,7 +1422,6 @@ export class Datetime implements ComponentInterface {
const result = getCombinedDateColumnData(
locale,
workingParts,
todayParts,
min,
max,
@@ -1537,7 +1552,7 @@ export class Datetime implements ComponentInterface {
const shouldRenderYears = forcePresentation !== 'month' && forcePresentation !== 'time';
const years = shouldRenderYears
? getYearColumnData(this.todayParts, this.minParts, this.maxParts, this.parsedYearValues)
? getYearColumnData(this.locale, this.todayParts, this.minParts, this.maxParts, this.parsedYearValues)
: [];
/**
@@ -1850,11 +1865,11 @@ export class Datetime implements ComponentInterface {
<div class="calendar-next-prev">
<ion-buttons>
<ion-button disabled={prevMonthDisabled} onClick={() => this.prevMonth()}>
<ion-icon slot="icon-only" icon={chevronBack} lazy={false} flipRtl></ion-icon>
<ion-button aria-label="previous month" disabled={prevMonthDisabled} onClick={() => this.prevMonth()}>
<ion-icon aria-hidden="true" slot="icon-only" icon={chevronBack} lazy={false} flipRtl></ion-icon>
</ion-button>
<ion-button disabled={nextMonthDisabled} onClick={() => this.nextMonth()}>
<ion-icon slot="icon-only" icon={chevronForward} lazy={false} flipRtl></ion-icon>
<ion-button aria-label="next month" disabled={nextMonthDisabled} onClick={() => this.nextMonth()}>
<ion-icon aria-hidden="true" slot="icon-only" icon={chevronForward} lazy={false} flipRtl></ion-icon>
</ion-button>
</ion-buttons>
</div>
@@ -1904,7 +1919,7 @@ export class Datetime implements ComponentInterface {
const { day, dayOfWeek } = dateObject;
const { isDateEnabled, multiple } = this;
const referenceParts = { month, day, year };
const { isActive, isToday, ariaLabel, ariaSelected, disabled } = getCalendarDayState(
const { isActive, isToday, ariaLabel, ariaSelected, disabled, text } = getCalendarDayState(
this.locale,
referenceParts,
this.activePartsClone,
@@ -1981,7 +1996,7 @@ export class Datetime implements ComponentInterface {
}
}}
>
{day}
{text}
</button>
);
})}
@@ -2000,7 +2015,7 @@ export class Datetime implements ComponentInterface {
}
private renderCalendar(mode: Mode) {
return (
<div class="datetime-calendar">
<div class="datetime-calendar" key="datetime-calendar">
{this.renderCalendarHeader(mode)}
{this.renderCalendarBody()}
</div>

View File

@@ -1,4 +1,4 @@
import { generateMonths, getDaysOfWeek, generateTime, getToday } from '../utils/data';
import { generateMonths, getDaysOfWeek, generateTime, getToday, getCombinedDateColumnData } from '../utils/data';
describe('generateMonths()', () => {
it('should generate correct month data', () => {
@@ -342,3 +342,25 @@ describe('getToday', () => {
expect(res).toEqual('2022-02-21T18:30:00.000Z');
});
});
describe('getCombinedDateColumnData', () => {
it('should return correct data with dates across years', () => {
const { parts, items } = getCombinedDateColumnData(
'en-US',
{ day: 1, month: 1, year: 2021 },
{ day: 31, month: 12, year: 2020 },
{ day: 2, month: 1, year: 2021 }
);
expect(parts).toEqual([
{ month: 12, year: 2020, day: 31 },
{ month: 1, year: 2021, day: 1 },
{ month: 1, year: 2021, day: 2 },
]);
expect(items).toEqual([
{ text: 'Thu, Dec 31', value: '2020-12-31' },
{ text: 'Today', value: '2021-1-1' },
{ text: 'Sat, Jan 2', value: '2021-1-2' },
]);
});
});

View File

@@ -155,4 +155,17 @@ describe('getLocalizedTime', () => {
expect(getLocalizedTime('en-US', datetimeParts, false)).toEqual('9:40 AM');
});
it('should avoid Chromium bug when using 12 hour time in a 24 hour locale', () => {
const datetimeParts = {
day: 1,
month: 1,
year: 2022,
hour: 0,
minute: 0,
tzOffset: 0,
};
expect(getLocalizedTime('en-GB', datetimeParts, false)).toEqual('12:00 am');
});
});

View File

@@ -62,6 +62,26 @@ test.describe('datetime: locale', () => {
test('time picker should not have visual regressions', async () => {
await datetimeFixture.expectLocalizedTimePicker();
});
test('should correctly localize calendar day buttons without literal', async ({ page }) => {
await page.setContent(`
<ion-datetime locale="ja-JP" presentation="date" value="2022-01-01"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const datetimeButtons = page.locator('ion-datetime .calendar-day:not([disabled])');
/**
* Note: The Intl.DateTimeFormat typically adds literals
* for certain languages. For Japanese, that could look
* something like "29日". However, we only want the "29"
* to be shown.
*/
await expect(datetimeButtons.nth(0)).toHaveText('1');
await expect(datetimeButtons.nth(1)).toHaveText('2');
await expect(datetimeButtons.nth(2)).toHaveText('3');
});
});
test.describe('es-ES', () => {
@@ -83,6 +103,40 @@ test.describe('datetime: locale', () => {
});
});
test.describe('ar-EG', () => {
test.beforeEach(async ({ skip }) => {
skip.rtl();
skip.mode('md');
});
test('should correctly localize calendar day buttons', async ({ page }) => {
await page.setContent(`
<ion-datetime locale="ar-EG" presentation="date" value="2022-01-01"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const datetimeButtons = page.locator('ion-datetime .calendar-day:not([disabled])');
await expect(datetimeButtons.nth(0)).toHaveText('١');
await expect(datetimeButtons.nth(1)).toHaveText('٢');
await expect(datetimeButtons.nth(2)).toHaveText('٣');
});
test('should correctly localize year column data', async ({ page }) => {
await page.setContent(`
<ion-datetime prefer-wheel="true" locale="ar-EG" presentation="date" value="2022-01-01"></ion-datetime>
`);
await page.waitForSelector('.datetime-ready');
const datetimeYears = page.locator('ion-datetime .year-column .picker-item:not(.picker-item-empty)');
await expect(datetimeYears.nth(0)).toHaveText('٢٠٢٢');
await expect(datetimeYears.nth(1)).toHaveText('٢٠٢١');
await expect(datetimeYears.nth(2)).toHaveText('٢٠٢٠');
});
});
class DatetimeLocaleFixture {
readonly page: E2EPage;
locale = 'en-US';

View File

@@ -111,27 +111,81 @@ test.describe('datetime: minmax', () => {
});
test.describe('setting value outside bounds should show in-bounds month', () => {
const testDisplayedMonth = async (page: E2EPage, content: string) => {
test.beforeEach(({ skip }) => {
skip.rtl();
});
const testDisplayedMonth = async (page: E2EPage, content: string, expectedString = 'June 2021') => {
await page.setContent(content);
await page.waitForSelector('.datetime-ready');
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
await expect(calendarMonthYear).toHaveText('June 2021');
await expect(calendarMonthYear).toHaveText(expectedString);
};
test('when min is defined', async ({ page }) => {
test('when min and value are defined', async ({ page }) => {
await testDisplayedMonth(page, `<ion-datetime min="2021-06-01" value="2021-05-01"></ion-datetime>`);
});
test('when max is defined', async ({ page }) => {
test('when max and value are defined', async ({ page }) => {
await testDisplayedMonth(page, `<ion-datetime max="2021-06-30" value="2021-07-01"></ion-datetime>`);
});
test('when both min and max are defined', async ({ page }) => {
test('when min, max, and value are defined', async ({ page }) => {
await testDisplayedMonth(
page,
`<ion-datetime min="2021-06-01" max="2021-06-30" value="2021-05-01"></ion-datetime>`
);
});
test('when max is defined', async ({ page }) => {
await testDisplayedMonth(page, `<ion-datetime max="2012-06-01"></ion-datetime>`, 'June 2012');
});
});
// TODO(FW-2165)
test('should not loop infinitely in webkit', async ({ page, skip }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/25752',
});
skip.browser('chromium');
skip.browser('firefox');
await page.setContent(`
<button id="bind">Bind datetimeMonthDidChange event</button>
<ion-datetime min="2022-04-15" value="2022-04-20" presentation="date" locale="en-US"></ion-datetime>
<script type="module">
import { InitMonthDidChangeEvent } from '/src/components/datetime/test/utils/month-did-change-event.js';
document.querySelector('#bind').addEventListener('click', function() {
InitMonthDidChangeEvent();
});
</script>
`);
await page.waitForSelector('.datetime-ready');
const datetimeMonthDidChange = await page.spyOnEvent('datetimeMonthDidChange');
const eventButton = page.locator('button#bind');
await eventButton.click();
const buttons = page.locator('ion-datetime .calendar-next-prev ion-button');
await buttons.nth(1).click();
await page.waitForChanges();
await datetimeMonthDidChange.next();
/**
* This is hacky, but its purpose is to make sure
* we are not triggering a WebKit bug. When the fix
* for the bug ships in WebKit, this will be removed.
*/
await page.evaluate(() => {
return new Promise((resolve) => {
setTimeout(resolve, 500);
});
});
await expect(datetimeMonthDidChange).toHaveReceivedEventTimes(1);
});
});

View File

@@ -1,4 +1,4 @@
import { clampDate, getPartsFromCalendarDay, parseAmPm } from '../utils/parse';
import { clampDate, getPartsFromCalendarDay, parseAmPm, parseMinParts, parseMaxParts } from '../utils/parse';
describe('getPartsFromCalendarDay()', () => {
it('should extract DatetimeParts from a calendar day element', () => {
@@ -72,3 +72,89 @@ describe('parseAmPm()', () => {
expect(parseAmPm(11)).toEqual('am');
});
});
describe('parseMinParts()', () => {
it('should fill in missing information when not provided', () => {
const today = {
day: 14,
month: 3,
year: 2022,
minute: 4,
hour: 2,
};
expect(parseMinParts('2012', today)).toEqual({
month: 1,
day: 1,
year: 2012,
hour: 0,
minute: 0,
});
});
it('should default to current year when only given HH:mm', () => {
const today = {
day: 14,
month: 3,
year: 2022,
minute: 4,
hour: 2,
};
expect(parseMinParts('04:30', today)).toEqual({
month: 1,
day: 1,
year: 2022,
hour: 4,
minute: 30,
});
});
});
describe('parseMaxParts()', () => {
it('should fill in missing information when not provided', () => {
const today = {
day: 14,
month: 3,
year: 2022,
minute: 4,
hour: 2,
};
expect(parseMaxParts('2012', today)).toEqual({
month: 12,
day: 31,
year: 2012,
hour: 23,
minute: 59,
});
});
it('should default to current year when only given HH:mm', () => {
const today = {
day: 14,
month: 3,
year: 2022,
minute: 4,
hour: 2,
};
expect(parseMaxParts('04:30', today)).toEqual({
month: 12,
day: 31,
year: 2022,
hour: 4,
minute: 30,
});
});
it('should fill in correct day during a leap year', () => {
const today = {
day: 14,
month: 3,
year: 2022,
minute: 4,
hour: 2,
};
expect(parseMaxParts('2012-02', today)).toEqual({
month: 2,
day: 29,
year: 2012,
hour: 23,
minute: 59,
});
});
});

View File

@@ -251,7 +251,7 @@ test.describe('datetime: prefer wheel', () => {
const dateValues = page.locator('.date-column .picker-item:not(.picker-item-empty)');
expect(await dateValues.count()).toBe(427);
expect(await dateValues.count()).toBe(397);
});
});
test.describe('datetime: time-date wheel rendering', () => {
@@ -356,7 +356,7 @@ test.describe('datetime: prefer wheel', () => {
const dateValues = page.locator('.date-column .picker-item:not(.picker-item-empty)');
expect(await dateValues.count()).toBe(427);
expect(await dateValues.count()).toBe(397);
});
});
});

View File

@@ -12,6 +12,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: null,
ariaLabel: 'Tuesday, January 1',
text: '1',
});
expect(getCalendarDayState('en-US', refA, refA, refC)).toEqual({
@@ -20,6 +21,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: 'true',
ariaLabel: 'Tuesday, January 1',
text: '1',
});
expect(getCalendarDayState('en-US', refA, refB, refA)).toEqual({
@@ -28,6 +30,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: null,
ariaLabel: 'Today, Tuesday, January 1',
text: '1',
});
expect(getCalendarDayState('en-US', refA, refA, refA)).toEqual({
@@ -36,6 +39,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: 'true',
ariaLabel: 'Today, Tuesday, January 1',
text: '1',
});
expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [1])).toEqual({
@@ -44,6 +48,7 @@ describe('getCalendarDayState()', () => {
disabled: false,
ariaSelected: 'true',
ariaLabel: 'Today, Tuesday, January 1',
text: '1',
});
expect(getCalendarDayState('en-US', refA, refA, refA, undefined, undefined, [2])).toEqual({
@@ -52,6 +57,7 @@ describe('getCalendarDayState()', () => {
disabled: true,
ariaSelected: 'true',
ariaLabel: 'Today, Tuesday, January 1',
text: '1',
});
});
});

View File

@@ -3,7 +3,14 @@ import type { PickerColumnItem } from '../../picker-column-internal/picker-colum
import type { DatetimeParts } from '../datetime-interface';
import { isAfter, isBefore, isSameDay } from './comparison';
import { getLocalizedDayPeriod, removeDateTzOffset, getFormattedHour, addTimePadding, getTodayLabel } from './format';
import {
getLocalizedDayPeriod,
removeDateTzOffset,
getFormattedHour,
addTimePadding,
getTodayLabel,
getYear,
} from './format';
import { getNumDaysInMonth, is24Hour } from './helpers';
import { getNextMonth, getPreviousMonth, getInternalHourValue } from './manipulation';
@@ -384,6 +391,7 @@ export const getDayColumnData = (
};
export const getYearColumnData = (
locale: string,
refParts: DatetimeParts,
minParts?: DatetimeParts,
maxParts?: DatetimeParts,
@@ -409,7 +417,7 @@ export const getYearColumnData = (
}
return processedYears.map((year) => ({
text: `${year}`,
text: getYear(locale, { year, month: refParts.month, day: refParts.day }),
value: year,
}));
};
@@ -439,7 +447,6 @@ const getAllMonthsInRange = (currentParts: DatetimeParts, maxParts: DatetimePart
*/
export const getCombinedDateColumnData = (
locale: string,
refParts: DatetimeParts,
todayParts: DatetimeParts,
minParts: DatetimeParts,
maxParts: DatetimeParts,
@@ -471,7 +478,7 @@ export const getCombinedDateColumnData = (
* of work as the text.
*/
months.forEach((monthObject) => {
const referenceMonth = { month: monthObject.month, day: null, year: refParts.year };
const referenceMonth = { month: monthObject.month, day: null, year: monthObject.year };
const monthDays = getDayColumnData(locale, referenceMonth, minParts, maxParts, dayValues, {
month: 'short',
day: 'numeric',
@@ -490,7 +497,7 @@ export const getCombinedDateColumnData = (
*/
dateColumnItems.push({
text: isToday ? getTodayLabel(locale) : dayObject.text,
value: `${refParts.year}-${monthObject.month}-${dayObject.value}`,
value: `${referenceMonth.year}-${referenceMonth.month}-${dayObject.value}`,
});
/**
@@ -504,8 +511,8 @@ export const getCombinedDateColumnData = (
* updating the picker column value.
*/
dateParts.push({
month: monthObject.month,
year: refParts.year,
month: referenceMonth.month,
year: referenceMonth.year,
day: dayObject.value as number,
});
});

View File

@@ -19,7 +19,11 @@ export const getLocalizedTime = (locale: string, refParts: DatetimeParts, use24H
hour: 'numeric',
minute: 'numeric',
timeZone: 'UTC',
hour12: !use24Hour,
/**
* 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',
}).format(
new Date(
convertDataToISO({
@@ -119,20 +123,73 @@ export const getMonthDayAndYear = (locale: string, refParts: DatetimeParts) => {
};
/**
* Wrapper function for Intl.DateTimeFormat.
* Allows developers to apply an allowed format to DatetimeParts.
* This function also has built in safeguards for older browser bugs
* with Intl.DateTimeFormat.
* Given a locale and a date object,
* return a formatted string that includes
* the numeric day.
* Note: Some languages will add literal characters
* to the end. This function removes those literals.
* Example: 29
*/
export const getDay = (locale: string, refParts: DatetimeParts) => {
return getLocalizedDateTimeParts(locale, refParts, { day: 'numeric' }).find((obj) => obj.type === 'day')!.value;
};
/**
* Given a locale and a date object,
* return a formatted string that includes
* the numeric year.
* Example: 2022
*/
export const getYear = (locale: string, refParts: DatetimeParts) => {
return getLocalizedDateTime(locale, refParts, { year: 'numeric' });
};
const getNormalizedDate = (refParts: DatetimeParts) => {
const timeString = !!refParts.hour && !!refParts.minute ? ` ${refParts.hour}:${refParts.minute}` : '';
return new Date(`${refParts.month}/${refParts.day}/${refParts.year}${timeString} GMT+0000`);
};
/**
* Given a locale, DatetimeParts, and options
* format the DatetimeParts according to the options
* and locale combination. This returns a string. If
* you want an array of the individual pieces
* that make up the localized date string, use
* getLocalizedDateTimeParts.
*/
export const getLocalizedDateTime = (
locale: string,
refParts: DatetimeParts,
options: Intl.DateTimeFormatOptions
): string => {
const timeString =
refParts.hour !== undefined && refParts.minute !== undefined ? ` ${refParts.hour}:${refParts.minute}` : '';
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}${timeString} GMT+0000`);
return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).format(date);
const date = getNormalizedDate(refParts);
return getDateTimeFormat(locale, options).format(date);
};
/**
* Given a locale, DatetimeParts, and options
* format the DatetimeParts according to the options
* and locale combination. This returns an array of
* each piece of the date.
*/
export const getLocalizedDateTimeParts = (
locale: string,
refParts: DatetimeParts,
options: Intl.DateTimeFormatOptions
): Intl.DateTimeFormatPart[] => {
const date = getNormalizedDate(refParts);
return getDateTimeFormat(locale, options).formatToParts(date);
};
/**
* Wrapper function for Intl.DateTimeFormat.
* Allows developers to apply an allowed format to DatetimeParts.
* This function also has built in safeguards for older browser bugs
* with Intl.DateTimeFormat.
*/
const getDateTimeFormat = (locale: string, options: Intl.DateTimeFormatOptions) => {
return new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' });
};
/**

View File

@@ -1,6 +1,7 @@
import type { DatetimeParts } from '../datetime-interface';
import { isAfter, isBefore } from './comparison';
import { getNumDaysInMonth } from './helpers';
const ISO_8601_REGEXP =
// eslint-disable-next-line no-useless-escape
@@ -138,3 +139,72 @@ export const clampDate = (
export const parseAmPm = (hour: number) => {
return hour >= 12 ? 'pm' : 'am';
};
/**
* Takes a max date string and creates a DatetimeParts
* object, filling in any missing information.
* For example, max="2012" would fill in the missing
* month, day, hour, and minute information.
*/
export const parseMaxParts = (max: string, todayParts: DatetimeParts): DatetimeParts => {
const { month, day, year, hour, minute } = parseDate(max);
/**
* When passing in `max` or `min`, developers
* can pass in any ISO-8601 string. This means
* that not all of the date/time fields are defined.
* For example, passing max="2012" is valid even though
* there is no month, day, hour, or minute data.
* However, all of this data is required when clamping the date
* so that the correct initial value can be selected. As a result,
* we need to fill in any omitted data with the min or max values.
*/
const yearValue = year ?? todayParts.year;
const monthValue = month ?? 12;
return {
month: monthValue,
day: day ?? getNumDaysInMonth(monthValue, yearValue),
/**
* Passing in "HH:mm" is a valid ISO-8601
* string, so we just default to the current year
* in this case.
*/
year: yearValue,
hour: hour ?? 23,
minute: minute ?? 59,
};
};
/**
* Takes a min date string and creates a DatetimeParts
* object, filling in any missing information.
* For example, min="2012" would fill in the missing
* month, day, hour, and minute information.
*/
export const parseMinParts = (min: string, todayParts: DatetimeParts): DatetimeParts => {
const { month, day, year, hour, minute } = parseDate(min);
/**
* When passing in `max` or `min`, developers
* can pass in any ISO-8601 string. This means
* that not all of the date/time fields are defined.
* For example, passing max="2012" is valid even though
* there is no month, day, hour, or minute data.
* However, all of this data is required when clamping the date
* so that the correct initial value can be selected. As a result,
* we need to fill in any omitted data with the min or max values.
*/
return {
month: month ?? 1,
day: day ?? 1,
/**
* Passing in "HH:mm" is a valid ISO-8601
* string, so we just default to the current year
* in this case.
*/
year: year ?? todayParts.year,
hour: hour ?? 0,
minute: minute ?? 0,
};
};

View File

@@ -1,7 +1,7 @@
import type { DatetimeParts } from '../datetime-interface';
import { isAfter, isBefore, isSameDay } from './comparison';
import { generateDayAriaLabel } from './format';
import { generateDayAriaLabel, getDay } from './format';
import { getNextMonth, getPreviousMonth } from './manipulation';
export const isYearDisabled = (refYear: number, minParts?: DatetimeParts, maxParts?: DatetimeParts) => {
@@ -123,6 +123,7 @@ export const getCalendarDayState = (
isToday,
ariaSelected: isActive ? 'true' : null,
ariaLabel: generateDayAriaLabel(locale, isToday, refParts),
text: refParts.day != null ? getDay(locale, refParts) : null,
};
};