Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1cb8fff
feat: implement second calendar
GDamyanov Feb 2, 2026
f033bf1
feat: create resize handler
GDamyanov Feb 2, 2026
bf64d21
feat: add modals for other pickers
GDamyanov Feb 3, 2026
53c4c20
feat: enhance calendar header in multiple months mode
GDamyanov Feb 24, 2026
dd82ece
feat: show pickers as overlay
GDamyanov Feb 25, 2026
9794c43
fix: style overlays pickers
GDamyanov Feb 25, 2026
714007c
fix: add year range button
GDamyanov Feb 25, 2026
3d2e44f
feat: style pickers
GDamyanov Feb 26, 2026
9188576
feat: style header buttons
GDamyanov Mar 4, 2026
a52202a
feat: start to implement mobile footer
GDamyanov Mar 5, 2026
0ea3a31
Merge branch 'main' into twocalendars
GDamyanov Mar 5, 2026
945568f
feat: create mobile footer
GDamyanov Mar 5, 2026
7cda354
Merge branch 'main' into twocalendars
GDamyanov Mar 5, 2026
d4cfeb5
Merge branch 'main' into twocalendars
GDamyanov Mar 5, 2026
3d12c7d
feat: finish mobile view
GDamyanov Mar 5, 2026
343eac2
Merge branch 'main' into twocalendars
GDamyanov Mar 6, 2026
43a608f
fix: style overlay popup
GDamyanov Mar 6, 2026
4e5c571
refactor: extract duplicated code in template
GDamyanov Mar 6, 2026
2c4d4b3
test: added tests
GDamyanov Mar 6, 2026
122aaff
Merge branch 'main' into twocalendars
GDamyanov Mar 9, 2026
aecc1a0
feat: add classes
GDamyanov Mar 9, 2026
2522faa
feat: style overlay picker containers
GDamyanov Mar 9, 2026
2bd5da3
Merge branch 'main' into twocalendars
GDamyanov Mar 9, 2026
c0d44e4
fix: lint errors
GDamyanov Mar 9, 2026
f228db4
refactor: extract the logic for the header in header template
GDamyanov Mar 9, 2026
a4c87bc
Merge branch 'main' into twocalendars
GDamyanov Mar 9, 2026
49169e3
fix: lint errors
GDamyanov Mar 9, 2026
b6a3d0a
docs: add sample
GDamyanov Mar 9, 2026
da05f34
refactor: remove redundant text
GDamyanov Mar 10, 2026
23785d0
Merge branch 'main' into twocalendars
GDamyanov Mar 10, 2026
5518c95
fix: add again portrait mode
GDamyanov Mar 10, 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
465 changes: 465 additions & 0 deletions packages/main/cypress/specs/Calendar.cy.tsx

Large diffs are not rendered by default.

480 changes: 479 additions & 1 deletion packages/main/cypress/specs/DateRangePicker.cy.tsx

Large diffs are not rendered by default.

155 changes: 154 additions & 1 deletion packages/main/src/Calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ import type CalendarLegend from "./CalendarLegend.js";
import type { CalendarLegendItemSelectionChangeEventDetail } from "./CalendarLegend.js";
import type SpecialCalendarDate from "./SpecialCalendarDate.js";
import type CalendarLegendItemType from "./types/CalendarLegendItemType.js";
import { isPhone } from "@ui5/webcomponents-base/dist/Device.js";
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";

// Default calendar for bundling
import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js";
Expand Down Expand Up @@ -64,6 +67,8 @@ import {
} from "./generated/i18n/i18n-defaults.js";
import type { YearRangePickerChangeEventDetail } from "./YearRangePicker.js";

const PHONE_MODE_BREAKPOINT = 640; // px

interface ICalendarPicker extends HTMLElement {
_showPreviousPage: () => void,
_showNextPage: () => void,
Expand Down Expand Up @@ -280,6 +285,19 @@ class Calendar extends CalendarPart {
@property({ type: Boolean })
hideWeekNumbers = false;

/**
* Defines the number of months to display side by side in day picker view.
* Only applicable when selection mode is "Range".
* @default false
* @private
* @since 2.0.0
*/
@property({ type: Boolean })
_showTwoCalendars = false;

@property({ type: Boolean })
stretch = false;

/**
* Which picker is currently visible to the user: day/month/year/yearRange
* @private
Expand Down Expand Up @@ -311,6 +329,9 @@ class Calendar extends CalendarPart {
@property({ noAttribute: true })
_pickersMode: `${CalendarPickersMode}` = "DAY_MONTH_YEAR";

@property({ type: Boolean })
_isPortraitMode = false;

_valueIsProcessed = false;

_rangeStartYear?: number;
Expand Down Expand Up @@ -357,13 +378,128 @@ class Calendar extends CalendarPart {
@property()
_selectedItemType: `${CalendarLegendItemType}` = "None";

@property({ type: Boolean })
_phoneMode = false;

@property({ type: Boolean })
_portraitMode = false;

_handleResizeBound: ResizeObserverCallback;

@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;

constructor() {
super();

this._valueIsProcessed = false;
this._handleResizeBound = this._handleResize.bind(this);
}

onEnterDOM() {
ResizeHandler.register(document.body, this._handleResizeBound);
// Initialize modes on first load
this._handleResize();
}

get _phoneView() {
return isPhone() || this._phoneMode;
}

get _portraitView() {
return this._portraitMode;
}

/**
* Handles document resize to switch between `phoneMode` and `portraitMode`.
* - `_phoneMode`: Only when it's an actual phone device (isPhone() returns true)
* - `_portraitMode`: When resolution is under PHONE_MODE_BREAKPOINT (regardless of device type)
*/
_handleResize() {
const documentWidth = document.body.offsetWidth;
const underBreakpoint = documentWidth <= PHONE_MODE_BREAKPOINT;

// Phone mode: only when it's an actual phone device
const phoneModeChange = (underBreakpoint && !this._phoneMode) || (!underBreakpoint && this._phoneMode);

if (phoneModeChange) {
this._phoneMode = underBreakpoint;
}

// Portrait mode: when resolution is under breakpoint (can be tablet, desktop in narrow window, etc.)
const toPortraitMode = underBreakpoint;
const portraitModeChange = (toPortraitMode && !this._portraitMode) || (!toPortraitMode && this._portraitMode);

if (portraitModeChange) {
this._portraitMode = toPortraitMode;
}
}

onExitDOM() {
ResizeHandler.deregister(document.body, this._handleResizeBound);
}

/**
* Returns the timestamp for a specific month index when displaying multiple months
* @private
*/
_getMonthTimestamp(monthIndex: number): number {
if (monthIndex === 0) {
return this._timestamp;
}

const calendarDate = CalendarDateComponent.fromTimestamp(this._timestamp * 1000, this._primaryCalendarType);

// Set day to 1 to avoid day-of-month overflow issues
// (e.g., Jan 31 + 1 month would overflow to March if Feb doesn't have 31 days)
calendarDate.setDate(1);

// Add months one by one to handle month boundaries correctly
for (let i = 0; i < monthIndex; i++) {
const currentMonth = calendarDate.getMonth();
const currentYear = calendarDate.getYear();

if (currentMonth === 11) {
// December -> January of next year
calendarDate.setYear(currentYear + 1);
calendarDate.setMonth(0);
} else {
// Just increment the month
calendarDate.setMonth(currentMonth + 1);
}
}

return calendarDate.valueOf() / 1000;
}

/**
* Generates header button text (month and year) for a specific month timestamp
* @private
*/
_getHeaderTextForMonth(monthTimestamp: number): { monthText: string, yearText: string, secondMonthText?: string, secondYearText?: string } {
const calendarDate = CalendarDateComponent.fromTimestamp(monthTimestamp * 1000, this._primaryCalendarType);
const localeData = getCachedLocaleDataInstance(getLocale());
const yearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this.primaryCalendarType });

const monthText = localeData.getMonthsStandAlone("wide", this.primaryCalendarType)[calendarDate.getMonth()];
const localDate = calendarDate.toLocalJSDate();
const yearText = String(yearFormat.format(localDate, true));

const result: { monthText: string, yearText: string, secondMonthText?: string, secondYearText?: string } = {
monthText,
yearText,
};

if (this.hasSecondaryCalendarType) {
const secondaryDate = transformDateToSecondaryType(this.primaryCalendarType, this._secondaryCalendarType, monthTimestamp, true);
const secondaryCalendarDate = secondaryDate.firstDate || secondaryDate.lastDate;
const secondaryLocaleData = getCachedLocaleDataInstance(getLocale());
result.secondMonthText = secondaryLocaleData.getMonthsStandAlone("wide", this._secondaryCalendarType)[secondaryCalendarDate.getMonth()];
const secondaryYearFormat = DateFormat.getDateInstance({ format: "y", calendarType: this._secondaryCalendarType });
result.secondYearText = String(secondaryYearFormat.format(secondaryCalendarDate.toLocalJSDate(), true));
}

return result;
}

/**
Expand Down Expand Up @@ -675,12 +811,17 @@ class Calendar extends CalendarPart {
};
}

get _monthsToShow() {
const monthsToShow = this._showTwoCalendars ? 2 : 1;
return isPhone() ? 1 : monthsToShow;
}

/**
* The month button is hidden when the month picker or year picker is shown
* @private
*/
get _isHeaderMonthButtonHidden(): boolean {
return this._currentPicker !== "day";
return this._showTwoCalendars ? this._currentPicker === "yearrange" || this._currentPicker === "year" : this._currentPicker !== "day";
}

/**
Expand All @@ -700,6 +841,10 @@ class Calendar extends CalendarPart {
}

get _isDayPickerHidden() {
// In multi-calendar mode (monthsToShow > 1), keep day pickers visible even when other pickers are shown
if (this._showTwoCalendars) {
return false;
}
return this._currentPicker !== "day";
}

Expand All @@ -715,6 +860,14 @@ class Calendar extends CalendarPart {
return this._currentPicker !== "yearrange";
}

get _isDefaultHeaderModeInMultipleMonths() {
return !this._isDayPickerHidden && this._isYearPickerHidden;
}

get _shouldShowOnePickerHeaderButtonInMultipleMonths() {
return !this._isDayPickerHidden && !this._isYearPickerHidden;
}

get _currentYearRange(): CalendarYearRangeT {
const rangeSize = this.hasSecondaryCalendarType ? 8 : 20;
const yearsOffset = this.hasSecondaryCalendarType ? 2 : 9;
Expand Down
Loading