Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
478544c
feat(datetime): add DatetimeRangeParts interface for range selection
OS-susmitabhowmik Oct 14, 2026
a507212
feat(datetime): add selectionMode prop and deprecate multiple
OS-susmitabhowmik Oct 14, 2026
1ab6897
feat(datetime): add range flags to getCalendarDayState
OS-susmitabhowmik Oct 14, 2026
bb61d21
feat(datetime): implement range selection logic
OS-susmitabhowmik Oct 14, 2026
15aa4ae
feat(datetime): update calendar rendering for range selection
OS-susmitabhowmik Oct 14, 2026
cca6ee5
feat(datetime): add range highlight track styles
OS-susmitabhowmik Oct 14, 2026
f03eb37
feat(datetime): update header and datetime-button for range display
OS-susmitabhowmik Oct 14, 2026
26b65a7
feat(datetime): disable confirm button until range is complete
OS-susmitabhowmik Oct 14, 2026
8eb4d4e
test(datetime): add range and range-styling test pages
OS-susmitabhowmik Oct 14, 2026
4d7abfc
fix(datetime): align range track styles with Figma design
OS-susmitabhowmik Oct 14, 2026
9385623
fix(datetime): fix range style issues found during Figma validation
OS-susmitabhowmik Oct 14, 2026
8236f7d
chore(datetime): update generated files for range selection API
OS-susmitabhowmik Oct 16, 2026
581d280
chore(): add updated snapshots
Ionitron Oct 16, 2026
9d70a28
Merge branch 'next' into ROU-12704-datepicker-range
OS-susmitabhowmik Oct 16, 2026
5353064
fix(datetime): fix lint error and update state spec for range fields
OS-susmitabhowmik Oct 16, 2026
405bac7
fix(toolbar): restore baseline snapshot from next branch
OS-susmitabhowmik Oct 16, 2026
e90b03c
Revert "fix(toolbar): restore baseline snapshot from next branch"
OS-susmitabhowmik Oct 16, 2026
1b0efef
fix(datetime): fix SCSS lint property order and disallowed-list errors
OS-susmitabhowmik Oct 16, 2026
e07c496
chore(): add updated snapshots
Ionitron Oct 16, 2026
2a7744b
fix(datetime): fix remaining SCSS lint order errors
OS-susmitabhowmik Oct 16, 2026
47aa109
fix(datetime): apply prettier formatting to range files
OS-susmitabhowmik Oct 16, 2026
2c61014
fix(datetime): use pill shapes for range on all themes and fix disabl…
OS-susmitabhowmik Oct 16, 2026
1dbcd39
Merge branch 'next' into ROU-12704-datepicker-range
OS-susmitabhowmik Oct 27, 2026
19d5c0e
fix(datetime): make month/year wheels navigation-only in range and mu…
OS-susmitabhowmik Oct 27, 2026
91d19ed
fix(datetime): pass border-radius values as separate arguments for RT…
OS-susmitabhowmik Oct 27, 2026
21d0dc2
fix(angular): add missing datetime props to standalone DATETIME_INPUT…
OS-susmitabhowmik Oct 27, 2026
5ee1d71
fix(datetime): append both range labels for single-day range aria-label
OS-susmitabhowmik Oct 27, 2026
d917bd2
fix(datetime): prevent confirm from closing overlay with incomplete r…
OS-susmitabhowmik Oct 27, 2026
c1859c3
test(datetime): add e2e and unit tests for range selection mode
OS-susmitabhowmik Oct 27, 2026
1ca8d38
refactor(datetime): simplify setActivePartsRange and rangeContainsDis…
OS-susmitabhowmik Oct 27, 2026
4516f4f
fix(datetime): remove unused variables in range e2e tests
OS-susmitabhowmik Oct 27, 2026
5b93e9a
fix(datetime): remove unused page param in readonly range test
OS-susmitabhowmik Oct 27, 2026
133ac77
style(datetime): fix formatting in range e2e test fixture
OS-susmitabhowmik Oct 27, 2026
621ecb0
fix(datetime): use dynamic selectors for no-value range e2e tests
OS-susmitabhowmik Oct 27, 2026
609386d
style(datetime): fix indentation and remove extra blank line in setAc…
OS-susmitabhowmik Oct 27, 2026
900707b
fix(datetime): remove unused datetime variable in ionChange range test
OS-susmitabhowmik Oct 27, 2026
e63b135
chore(): add updated snapshots
Ionitron Oct 27, 2026
782299f
fix(datetime): fix range track subpixel gaps and iOS height
OS-susmitabhowmik Oct 27, 2026
0f67fc5
chore(): add updated snapshots
Ionitron Oct 27, 2026
a1c43b3
fix(datetime): fix range track rendering on iOS
OS-susmitabhowmik Oct 27, 2026
e0550e4
chore(): add updated snapshots
Ionitron Oct 27, 2026
4e27f4e
fix(datetime): fix range track subpixel gaps and iOS centering
OS-susmitabhowmik Oct 27, 2026
ddf6701
chore(): add updated snapshots
Ionitron Oct 27, 2026
c889e4a
test(datetime): add e2e tests for range selection aria labels
OS-susmitabhowmik Oct 27, 2026
5c28e28
chore(datetime): remove outdated range e2e snapshots
OS-susmitabhowmik Oct 27, 2026
60e3a89
style(datetime): fix formatting in range track background
OS-susmitabhowmik Oct 27, 2026
047ec56
chore(): add updated snapshots
Ionitron Oct 27, 2026
e54e7b7
refactor(datetime-button): simplify range endDate lookup and remove u…
OS-susmitabhowmik Oct 27, 2026
f0d2627
fix(datetime): fix disabled opacity seams and empty range navigation
OS-susmitabhowmik Oct 27, 2026
3da13e0
chore(): add updated snapshots
Ionitron Oct 27, 2026
c53ed45
test(datetime): add ionic-md mode to range visual regression tests
OS-susmitabhowmik Oct 27, 2026
662f013
chore(): add updated snapshots
Ionitron Oct 28, 2026
1cb5bb9
fix(datetime): stop cancel() from clearing a confirmed range selection
OS-susmitabhowmik Oct 28, 2026
7aef96c
fix(datetime): scope disabled range grid opacity to ios/md themes only
OS-susmitabhowmik Oct 28, 2026
3a651d6
test(datetime): strengthen range tests to fail without their fixes
OS-susmitabhowmik Oct 28, 2026
1380043
chore(): add updated snapshots
Ionitron Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ ion-datetime,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "se
ion-datetime,prop,dayValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,prop,disabled,boolean,false,false,false
ion-datetime,prop,doneText,string,'Done',false,false
ion-datetime,prop,endDateLabel,string,'End date',false,false
ion-datetime,prop,firstDayOfWeek,number,0,false,false
ion-datetime,prop,formatOptions,undefined | { date: DateTimeFormatOptions; time?: DateTimeFormatOptions | undefined; } | { date?: DateTimeFormatOptions | undefined; time: DateTimeFormatOptions; },undefined,false,false
ion-datetime,prop,highlightedDates,((dateIsoString: string) => DatetimeHighlightStyle | undefined) | DatetimeHighlight[] | undefined,undefined,false,false
Expand All @@ -711,12 +712,14 @@ ion-datetime,prop,name,string,this.inputId,false,false
ion-datetime,prop,preferWheel,boolean,false,false,false
ion-datetime,prop,presentation,"date" | "date-time" | "month" | "month-year" | "time" | "time-date" | "year",'date-time',false,false
ion-datetime,prop,readonly,boolean,false,false,false
ion-datetime,prop,selectionMode,"multiple" | "range" | undefined,undefined,false,false
ion-datetime,prop,showAdjacentDays,boolean,false,false,false
ion-datetime,prop,showClearButton,boolean,false,false,false
ion-datetime,prop,showDefaultButtons,boolean,false,false,false
ion-datetime,prop,showDefaultTimeLabel,boolean,true,false,false
ion-datetime,prop,showDefaultTitle,boolean,false,false,false
ion-datetime,prop,size,"cover" | "fixed",'fixed',false,false
ion-datetime,prop,startDateLabel,string,'Start date',false,false
ion-datetime,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-datetime,prop,titleSelectedDatesFormatter,((selectedDates: string[]) => string) | undefined,undefined,false,false
ion-datetime,prop,value,null | string | string[] | undefined,undefined,false,false
Expand All @@ -735,6 +738,9 @@ ion-datetime,css-prop,--background-rgb,ios
ion-datetime,css-prop,--background-rgb,md
ion-datetime,css-prop,--focus-ring-color,ionic
ion-datetime,css-prop,--focus-ring-width,ionic
ion-datetime,css-prop,--range-background,ionic
ion-datetime,css-prop,--range-background,ios
ion-datetime,css-prop,--range-background,md
ion-datetime,css-prop,--title-color,ios
ion-datetime,css-prop,--title-color,md
ion-datetime,css-prop,--wheel-fade-background-rgb,ios
Expand All @@ -756,6 +762,10 @@ ion-datetime,part,month-year-button
ion-datetime,part,navigation-button
ion-datetime,part,next-button
ion-datetime,part,previous-button
ion-datetime,part,range-end
ion-datetime,part,range-selection
ion-datetime,part,range-start
ion-datetime,part,range-track
ion-datetime,part,time-button
ion-datetime,part,time-button active
ion-datetime,part,wheel
Expand Down
33 changes: 33 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,11 @@ export namespace Components {
* @default 'Done'
*/
"doneText": string;
/**
* The text shown in the datetime header when the end of the range is empty, also used by screen readers to announce the end date. Only applies when `selectionMode="range"`.
* @default 'End date'
*/
"endDateLabel": string;
/**
* The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday.
* @default 0
Expand Down Expand Up @@ -1218,6 +1223,7 @@ export namespace Components {
"monthValues"?: number[] | number | string;
/**
* If `true`, multiple dates can be selected at once. Only applies to `presentation="date"` and `preferWheel="false"`.
* @deprecated Use `selectionMode="multiple"` instead.
* @default false
*/
"multiple": boolean;
Expand Down Expand Up @@ -1246,6 +1252,10 @@ export namespace Components {
* @param startDate A valid [ISO-8601 string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format) to reset the datetime state to.
*/
"reset": (startDate?: string) => Promise<void>;
/**
* Controls the date selection behavior. `'multiple'` enables selecting multiple individual dates. `'range'` enables selecting a start and end date range. When unset, single-date selection is used. Range selection only works with `presentation="date"` and `preferWheel="false"`.
*/
"selectionMode"?: 'multiple' | 'range';
/**
* If `true`, the datetime calendar displays a six-week (42-day) layout, including days from the previous and next months to fill the grid. These adjacent days are selectable unless disabled.
* @default false
Expand Down Expand Up @@ -1276,6 +1286,11 @@ export namespace Components {
* @default 'fixed'
*/
"size": 'cover' | 'fixed';
/**
* The text shown in the datetime header when the start of the range is empty, also used by screen readers to announce the start date. Only applies when `selectionMode="range"`.
* @default 'Start date'
*/
"startDateLabel": string;
/**
* The theme determines the visual appearance of the component.
*/
Expand Down Expand Up @@ -7070,6 +7085,11 @@ declare namespace LocalJSX {
* @default 'Done'
*/
"doneText"?: string;
/**
* The text shown in the datetime header when the end of the range is empty, also used by screen readers to announce the end date. Only applies when `selectionMode="range"`.
* @default 'End date'
*/
"endDateLabel"?: string;
/**
* The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday.
* @default 0
Expand Down Expand Up @@ -7122,6 +7142,7 @@ declare namespace LocalJSX {
"monthValues"?: number[] | number | string;
/**
* If `true`, multiple dates can be selected at once. Only applies to `presentation="date"` and `preferWheel="false"`.
* @deprecated Use `selectionMode="multiple"` instead.
* @default false
*/
"multiple"?: boolean;
Expand Down Expand Up @@ -7173,6 +7194,10 @@ declare namespace LocalJSX {
* @default false
*/
"readonly"?: boolean;
/**
* Controls the date selection behavior. `'multiple'` enables selecting multiple individual dates. `'range'` enables selecting a start and end date range. When unset, single-date selection is used. Range selection only works with `presentation="date"` and `preferWheel="false"`.
*/
"selectionMode"?: 'multiple' | 'range';
/**
* If `true`, the datetime calendar displays a six-week (42-day) layout, including days from the previous and next months to fill the grid. These adjacent days are selectable unless disabled.
* @default false
Expand Down Expand Up @@ -7203,6 +7228,11 @@ declare namespace LocalJSX {
* @default 'fixed'
*/
"size"?: 'cover' | 'fixed';
/**
* The text shown in the datetime header when the start of the range is empty, also used by screen readers to announce the start date. Only applies when `selectionMode="range"`.
* @default 'Start date'
*/
"startDateLabel"?: string;
/**
* The theme determines the visual appearance of the component.
*/
Expand Down Expand Up @@ -10721,6 +10751,9 @@ declare namespace LocalJSX {
"locale": string;
"firstDayOfWeek": number;
"multiple": boolean;
"selectionMode": 'multiple' | 'range';
"startDateLabel": string;
"endDateLabel": string;
"value": string | string[] | null;
"showDefaultTitle": boolean;
"showDefaultButtons": boolean;
Expand Down
42 changes: 30 additions & 12 deletions core/src/components/datetime-button/datetime-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,16 @@ export class DatetimeButton implements ComponentInterface {
return;
}

const { value, locale, formatOptions, hourCycle, preferWheel, multiple, titleSelectedDatesFormatter } = datetimeEl;
const {
value,
locale,
formatOptions,
hourCycle,
preferWheel,
multiple,
selectionMode,
titleSelectedDatesFormatter,
} = datetimeEl;

const parsedValues = this.getParsedDateValues(value);

Expand All @@ -213,6 +222,9 @@ export class DatetimeButton implements ComponentInterface {
return;
}

const isRangeMode = selectionMode === 'range';
const isMultipleMode = selectionMode === 'multiple' || (selectionMode === undefined && multiple);

/**
* If developers incorrectly use multiple="true"
* with non "date" datetimes, then just select
Expand All @@ -222,17 +234,14 @@ export class DatetimeButton implements ComponentInterface {
*/
const firstParsedDatetime = parsedDatetimes[0];
const computedHourCycle = getHourCycle(locale, hourCycle);
const dateFormatOptions = formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' };

this.dateText = this.timeText = undefined;

switch (datetimePresentation) {
case 'date-time':
case 'time-date':
const dateText = getLocalizedDateTime(
locale,
firstParsedDatetime,
formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' }
);
const dateText = getLocalizedDateTime(locale, firstParsedDatetime, dateFormatOptions);
const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle, formatOptions?.time);
if (preferWheel) {
this.dateText = `${dateText} ${timeText}`;
Expand All @@ -242,7 +251,20 @@ export class DatetimeButton implements ComponentInterface {
}
break;
case 'date':
if (multiple && parsedValues.length !== 1) {
if (isRangeMode && parsedValues.length === 2) {
// Range mode: display "startDate - endDate"
const startDate = getLocalizedDateTime(locale, parsedDatetimes[0], dateFormatOptions);
const endDate = getLocalizedDateTime(locale, parsedDatetimes[1], dateFormatOptions);
let rangeText = `${startDate} - ${endDate}`;
if (titleSelectedDatesFormatter !== undefined) {
try {
rangeText = titleSelectedDatesFormatter(parsedValues);
} catch (e) {
printIonError('[ion-datetime-button] - Exception in provided `titleSelectedDatesFormatter`:', e);
}
}
this.dateText = rangeText;
} else if (isMultipleMode && parsedValues.length !== 1) {
let headerText = `${parsedValues.length} days`; // default/fallback for multiple selection
if (titleSelectedDatesFormatter !== undefined) {
try {
Expand All @@ -253,11 +275,7 @@ export class DatetimeButton implements ComponentInterface {
}
this.dateText = headerText;
} else {
this.dateText = getLocalizedDateTime(
locale,
firstParsedDatetime,
formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' }
);
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, dateFormatOptions);
}
break;
case 'time':
Expand Down
5 changes: 5 additions & 0 deletions core/src/components/datetime/datetime-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;

export type DatetimeHighlightCallback = (dateIsoString: string) => DatetimeHighlightStyle | undefined;

export interface DatetimeRangeParts {
start: DatetimeParts;
end?: DatetimeParts;
}

export type DatetimeHourCycle = 'h11' | 'h12' | 'h23' | 'h24';

/**
Expand Down
96 changes: 96 additions & 0 deletions core/src/components/datetime/datetime.common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@
:host .calendar-day-wrapper {
display: flex;

position: relative;

align-items: center;
justify-content: center;

Expand All @@ -285,6 +287,100 @@
min-height: 0;

overflow: visible;

/**
* Create a stacking context so the range track
* (::before with z-index: -1) stays behind the
* day button but above the host background.
*/
z-index: 0;
}

// Range Selection Track
// -----------------------------------

/**
* Each day in the range draws its own piece of the track with a
* ::before. In-range days fill the cell, while the start and end
* caps cover the half between the pill center and the next day.
*
* Grid columns are `1fr`, so cell edges land on fractional pixels
* and a cell's track can fall a hairline short of its neighbor's.
* Each day therefore extends 1px toward the next day in the range,
* which the next day paints over except inside such a gap.
*
* @prop --range-background: The background color of the range selection track.
*/
:host .calendar-day-wrapper-in-range::before,
:host .calendar-day-wrapper-range-start::before,
:host .calendar-day-wrapper-range-end::before {
position: absolute;

top: 50%;

inset-inline-start: 0;
inset-inline-end: 0;

height: var(--range-track-height);

transform: translateY(-50%);

/**
* `--range-background` is translucent by default, so it is layered
* over the datetime background to paint an opaque track. Where two
* days overlap, the color stays the same instead of doubling up
* into a darker line.
*/
background: linear-gradient(var(--range-background), var(--range-background)),
linear-gradient(var(--background), var(--background));

content: "";

z-index: -1;
}

:host .calendar-day-wrapper-range-start::before {
inset-inline-start: 50%;
}

:host .calendar-day-wrapper-range-end::before {
inset-inline-end: 50%;
}

/**
* Only extend toward a day that draws the track as well. The last
* day of a week is followed by the next row, and the next day is
* unpainted when it ends the selection or is disabled inside the
* range, so extending there would leave a 1px nub.
*/
:host .calendar-day-wrapper-in-range:not(:nth-child(7n)):has(+ .calendar-day-wrapper-in-range)::before,
:host .calendar-day-wrapper-in-range:not(:nth-child(7n)):has(+ .calendar-day-wrapper-range-end)::before,
:host .calendar-day-wrapper-range-start:not(:nth-child(7n)):has(+ .calendar-day-wrapper-in-range)::before,
:host .calendar-day-wrapper-range-start:not(:nth-child(7n)):has(+ .calendar-day-wrapper-range-end)::before {
inset-inline-end: -1px;
}

/**
* When a single day is both the start and end of the range,
* do not show a track since there is nothing to connect.
*/
:host .calendar-day-wrapper-range-start.calendar-day-wrapper-range-end::before {
display: none;
}

/**
* When the component is disabled, fade the whole grid once instead of
* fading each day. Fading a day makes it its own compositing group, so
* the 1px that each day extends toward the next would be painted twice
* and read as a darker seam. The day buttons drop their own disabled
* opacity so the grid is the only thing applying it.
*/
:host(.datetime-disabled.datetime-range) .calendar-month-grid {
opacity: 0.4;
}

:host(.datetime-disabled.datetime-range) .calendar-month-grid .calendar-day[disabled] {
opacity: 1;
}

/**
Expand Down
Loading
Loading