diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 443c04611cf..880f379e25c 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -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, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; +import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimeParts, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; import { SpinnerTypes } from "./components/spinner/spinner-configs"; import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; import { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface"; @@ -53,7 +53,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, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; +export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimeParts, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface"; export { SpinnerTypes } from "./components/spinner/spinner-configs"; export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface"; export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface"; @@ -961,6 +961,10 @@ export namespace Components { * Formatting options for dates and times. Should include a 'date' and/or 'time' object, each of which is of type [Intl.DateTimeFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options). */ "formatOptions"?: FormatOptions; + /** + * Returns the default parts the datetime falls back to when no value is set: today's date and time snapped to the closest value allowed by the component's constraints (`min`, `max`, and the `*Values` props). + */ + "getDefaultPart": () => Promise; /** * Used to apply custom text and background colors to specific dates. Can be either an array of objects containing ISO strings and colors, or a callback that receives an ISO string and returns the colors. Only applies to the `date`, `date-time`, and `time-date` presentations, with `preferWheel="false"`. */ diff --git a/core/src/components/datetime-button/datetime-button.tsx b/core/src/components/datetime-button/datetime-button.tsx index af0a77f88e0..4895318e89a 100644 --- a/core/src/components/datetime-button/datetime-button.tsx +++ b/core/src/components/datetime-button/datetime-button.tsx @@ -7,7 +7,6 @@ import { createColorClasses } from '@utils/theme'; import { getIonMode } from '../../global/ionic-global'; import type { Color } from '../../interface'; import type { DatetimePresentation } from '../datetime/datetime-interface'; -import { getToday } from '../datetime/utils/data'; import { getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format'; import { getHourCycle } from '../datetime/utils/helpers'; import { parseDate } from '../datetime/utils/parse'; @@ -125,7 +124,7 @@ export class DatetimeButton implements ComponentInterface { overlayEl.classList.add('ion-datetime-button-overlay'); } - componentOnReady(datetimeEl, () => { + componentOnReady(datetimeEl, async () => { const datetimePresentation = (this.datetimePresentation = datetimeEl.presentation || 'date-time'); /** @@ -138,7 +137,7 @@ export class DatetimeButton implements ComponentInterface { * to re-render the displayed * text in the buttons. */ - this.setDateTimeText(); + await this.setDateTimeText(); addEventListener(datetimeEl, 'ionValueChange', this.setDateTimeText); /** @@ -189,7 +188,7 @@ export class DatetimeButton implements ComponentInterface { * ion-datetime and then format it according * to the locale specified on ion-datetime. */ - private setDateTimeText = () => { + private setDateTimeText = async () => { const { datetimeEl, datetimePresentation } = this; if (!datetimeEl) { @@ -201,10 +200,12 @@ export class DatetimeButton implements ComponentInterface { const parsedValues = this.getParsedDateValues(value); /** - * Both ion-datetime and ion-datetime-button default - * to today's date and time if no value is set. + * Both ion-datetime and ion-datetime-button default to today's date and + * time if no value is set. We read the datetime's computed default so the + * button respects the same constraints (min, max, minuteValues, etc.) that + * the datetime applies to its own fallback, instead of using a raw "now". */ - const parsedDatetimes = parseDate(parsedValues.length > 0 ? parsedValues : [getToday()]); + const parsedDatetimes = parsedValues.length > 0 ? parseDate(parsedValues) : [await datetimeEl.getDefaultPart()]; if (!parsedDatetimes) { return; diff --git a/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts b/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts index 35088bce4a7..5e684c43e47 100644 --- a/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts +++ b/core/src/components/datetime-button/test/basic/datetime-button.e2e.ts @@ -344,4 +344,146 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(page.locator('ion-datetime-button')).toContainText('Thu, November 02 01:22 AM'); }); }); + + test.describe(title('datetime-button: datetime constraints'), () => { + const fixedTime = new Date('2026-06-18T17:54:54.518Z'); + + const dateFormat = new Intl.DateTimeFormat('en-US', { + weekday: 'short', + month: 'long', + day: '2-digit', + }); + const timeFormat = new Intl.DateTimeFormat('en-US', { + hour: '2-digit', + minute: '2-digit', + }); + + test.beforeEach(async ({ page }) => { + await page.clock.setFixedTime(fixedTime); + }); + + test('should default to exact current time with no constraints', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30183', + }); + + await page.setContent( + ` + + + + `, + config + ); + await page.locator('.datetime-ready').waitFor(); + + await expect(page.locator('#date-button')).toContainText(dateFormat.format(fixedTime)); + await expect(page.locator('#time-button')).toContainText(timeFormat.format(fixedTime)); + }); + + test('should obey minuteValues constraint', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30183', + }); + + await page.setContent( + ` + + + + `, + config + ); + await page.locator('.datetime-ready').waitFor(); + + const expectedTime = new Date(fixedTime); + expectedTime.setMinutes(0); + + await expect(page.locator('#time-button')).toContainText(timeFormat.format(expectedTime)); + }); + + test('should obey hourValues constraint', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30183', + }); + + await page.setContent( + ` + + + + `, + config + ); + await page.locator('.datetime-ready').waitFor(); + + const expectedTime = new Date(fixedTime); + expectedTime.setHours(0); + + await expect(page.locator('#time-button')).toContainText(timeFormat.format(expectedTime)); + }); + + test('should obey monthValues constraint', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30183', + }); + + await page.setContent( + ` + + + + `, + config + ); + await page.locator('.datetime-ready').waitFor(); + + const expectedTime = new Date(fixedTime); + expectedTime.setMonth(0); + + await expect(page.locator('#date-button')).toContainText(dateFormat.format(expectedTime)); + }); + }); }); diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index f36cd66718d..5a4900eb390 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -604,6 +604,18 @@ export class Datetime implements ComponentInterface { } } + /** + * Returns the default parts the datetime falls back to when no value is set: + * today's date and time snapped to the closest value allowed by the + * component's constraints (`min`, `max`, and the `*Values` props). + * + * @internal + */ + @Method() + async getDefaultPart(): Promise { + return this.defaultParts; + } + private warnIfIncorrectValueUsage = () => { const { multiple, value } = this; if (!multiple && Array.isArray(value)) { @@ -1495,17 +1507,16 @@ export class Datetime implements ComponentInterface { warnIfTimeZoneProvided(el, formatOptions); } - const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues)); - const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues)); - const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues)); - const yearValues = (this.parsedYearValues = convertToArrayOfNumbers(this.yearValues)); - const dayValues = (this.parsedDayValues = convertToArrayOfNumbers(this.dayValues)); - const todayParts = (this.todayParts = parseDate(getToday())!); this.processMinParts(); this.processMaxParts(); + const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues)); + const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues)); + const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues)); + const yearValues = (this.parsedYearValues = convertToArrayOfNumbers(this.yearValues)); + const dayValues = (this.parsedDayValues = convertToArrayOfNumbers(this.dayValues)); this.defaultParts = getClosestValidDate({ refParts: todayParts, monthValues,