mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
chore(): sync with main
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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' });
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user