Skip to content

Commit 044f358

Browse files
authored
fix(datetime-button): respect datetime constraints in initial text (#31218)
Issue number: resolves #30183
1 parent a767b31 commit 044f358

4 files changed

Lines changed: 166 additions & 7 deletions

File tree

core/src/components.d.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
1515
import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
1616
import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
1717
import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
18-
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
18+
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimeParts, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
1919
import { SpinnerTypes } from "./components/spinner/spinner-configs";
2020
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
2121
import { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface";
@@ -53,7 +53,7 @@ export { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
5353
export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
5454
export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
5555
export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
56-
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
56+
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimeParts, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
5757
export { SpinnerTypes } from "./components/spinner/spinner-configs";
5858
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
5959
export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface";
@@ -961,6 +961,10 @@ export namespace Components {
961961
* 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).
962962
*/
963963
"formatOptions"?: FormatOptions;
964+
/**
965+
* 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).
966+
*/
967+
"getDefaultPart": () => Promise<DatetimeParts>;
964968
/**
965969
* 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"`.
966970
*/

core/src/components/datetime-button/datetime-button.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { createColorClasses } from '@utils/theme';
77
import { getIonMode } from '../../global/ionic-global';
88
import type { Color } from '../../interface';
99
import type { DatetimePresentation } from '../datetime/datetime-interface';
10-
import { getToday } from '../datetime/utils/data';
1110
import { getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
1211
import { getHourCycle } from '../datetime/utils/helpers';
1312
import { parseDate } from '../datetime/utils/parse';
@@ -189,7 +188,7 @@ export class DatetimeButton implements ComponentInterface {
189188
* ion-datetime and then format it according
190189
* to the locale specified on ion-datetime.
191190
*/
192-
private setDateTimeText = () => {
191+
private setDateTimeText = async () => {
193192
const { datetimeEl, datetimePresentation } = this;
194193

195194
if (!datetimeEl) {
@@ -201,10 +200,12 @@ export class DatetimeButton implements ComponentInterface {
201200
const parsedValues = this.getParsedDateValues(value);
202201

203202
/**
204-
* Both ion-datetime and ion-datetime-button default
205-
* to today's date and time if no value is set.
203+
* Both ion-datetime and ion-datetime-button default to today's date and
204+
* time if no value is set. We read the datetime's computed default so the
205+
* button respects the same constraints (min, max, minuteValues, etc.) that
206+
* the datetime applies to its own fallback, instead of using a raw "now".
206207
*/
207-
const parsedDatetimes = parseDate(parsedValues.length > 0 ? parsedValues : [getToday()]);
208+
const parsedDatetimes = parsedValues.length > 0 ? parseDate(parsedValues) : [await datetimeEl.getDefaultPart()];
208209

209210
if (!parsedDatetimes) {
210211
return;

core/src/components/datetime-button/test/basic/datetime-button.e2e.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,146 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
344344
await expect(page.locator('ion-datetime-button')).toContainText('Thu, November 02 01:22 AM');
345345
});
346346
});
347+
348+
test.describe(title('datetime-button: datetime constraints'), () => {
349+
const fixedTime = new Date('2026-06-18T17:54:54.518Z');
350+
351+
const dateFormat = new Intl.DateTimeFormat('en-US', {
352+
weekday: 'short',
353+
month: 'long',
354+
day: '2-digit',
355+
});
356+
const timeFormat = new Intl.DateTimeFormat('en-US', {
357+
hour: '2-digit',
358+
minute: '2-digit',
359+
});
360+
361+
test.beforeEach(async ({ page }) => {
362+
await page.clock.setFixedTime(fixedTime);
363+
});
364+
365+
test('should default to exact current time with no constraints', async ({ page }, testInfo) => {
366+
testInfo.annotations.push({
367+
type: 'issue',
368+
description: 'https://github.com/ionic-team/ionic-framework/issues/30183',
369+
});
370+
371+
await page.setContent(
372+
`
373+
<ion-datetime-button datetime="datetime"></ion-datetime-button>
374+
<ion-datetime id="datetime" presentation="date-time" locale="en-US"></ion-datetime>
375+
<script>
376+
const datetime = document.querySelector('ion-datetime');
377+
datetime.formatOptions = {
378+
date: {
379+
weekday: "short",
380+
month: "long",
381+
day: "2-digit"
382+
},
383+
time: {
384+
hour: "2-digit",
385+
minute: "2-digit"
386+
}
387+
}
388+
</script>
389+
`,
390+
config
391+
);
392+
await page.locator('.datetime-ready').waitFor();
393+
394+
await expect(page.locator('#date-button')).toContainText(dateFormat.format(fixedTime));
395+
await expect(page.locator('#time-button')).toContainText(timeFormat.format(fixedTime));
396+
});
397+
398+
test('should obey minuteValues constraint', async ({ page }, testInfo) => {
399+
testInfo.annotations.push({
400+
type: 'issue',
401+
description: 'https://github.com/ionic-team/ionic-framework/issues/30183',
402+
});
403+
404+
await page.setContent(
405+
`
406+
<ion-datetime-button datetime="datetime"></ion-datetime-button>
407+
<ion-datetime id="datetime" presentation="time" locale="en-US" minute-values="0"></ion-datetime>
408+
<script>
409+
const datetime = document.querySelector('ion-datetime');
410+
datetime.formatOptions = {
411+
time: {
412+
hour: "2-digit",
413+
minute: "2-digit"
414+
}
415+
}
416+
</script>
417+
`,
418+
config
419+
);
420+
await page.locator('.datetime-ready').waitFor();
421+
422+
const expectedTime = new Date(fixedTime);
423+
expectedTime.setMinutes(0);
424+
425+
await expect(page.locator('#time-button')).toContainText(timeFormat.format(expectedTime));
426+
});
427+
428+
test('should obey hourValues constraint', async ({ page }, testInfo) => {
429+
testInfo.annotations.push({
430+
type: 'issue',
431+
description: 'https://github.com/ionic-team/ionic-framework/issues/30183',
432+
});
433+
434+
await page.setContent(
435+
`
436+
<ion-datetime-button datetime="datetime"></ion-datetime-button>
437+
<ion-datetime id="datetime" presentation="time" locale="en-US" hour-values="0"></ion-datetime>
438+
<script>
439+
const datetime = document.querySelector('ion-datetime');
440+
datetime.formatOptions = {
441+
time: {
442+
hour: "2-digit",
443+
minute: "2-digit"
444+
}
445+
}
446+
</script>
447+
`,
448+
config
449+
);
450+
await page.locator('.datetime-ready').waitFor();
451+
452+
const expectedTime = new Date(fixedTime);
453+
expectedTime.setHours(0);
454+
455+
await expect(page.locator('#time-button')).toContainText(timeFormat.format(expectedTime));
456+
});
457+
458+
test('should obey monthValues constraint', async ({ page }, testInfo) => {
459+
testInfo.annotations.push({
460+
type: 'issue',
461+
description: 'https://github.com/ionic-team/ionic-framework/issues/30183',
462+
});
463+
464+
await page.setContent(
465+
`
466+
<ion-datetime-button datetime="datetime"></ion-datetime-button>
467+
<ion-datetime id="datetime" presentation="date" locale="en-US" month-values="1"></ion-datetime>
468+
<script>
469+
const datetime = document.querySelector('ion-datetime');
470+
datetime.formatOptions = {
471+
date: {
472+
weekday: "short",
473+
month: "long",
474+
day: "2-digit"
475+
}
476+
}
477+
</script>
478+
`,
479+
config
480+
);
481+
await page.locator('.datetime-ready').waitFor();
482+
483+
const expectedTime = new Date(fixedTime);
484+
expectedTime.setMonth(0);
485+
486+
await expect(page.locator('#date-button')).toContainText(dateFormat.format(expectedTime));
487+
});
488+
});
347489
});

core/src/components/datetime/datetime.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,18 @@ export class Datetime implements ComponentInterface {
604604
}
605605
}
606606

607+
/**
608+
* Returns the default parts the datetime falls back to when no value is set:
609+
* today's date and time snapped to the closest value allowed by the
610+
* component's constraints (`min`, `max`, and the `*Values` props).
611+
*
612+
* @internal
613+
*/
614+
@Method()
615+
async getDefaultPart(): Promise<DatetimeParts> {
616+
return this.defaultParts;
617+
}
618+
607619
private warnIfIncorrectValueUsage = () => {
608620
const { multiple, value } = this;
609621
if (!multiple && Array.isArray(value)) {

0 commit comments

Comments
 (0)