diff --git a/packages/main/cypress/specs/Calendar.cy.tsx b/packages/main/cypress/specs/Calendar.cy.tsx index 60db3bae7491..960e362464a3 100644 --- a/packages/main/cypress/specs/Calendar.cy.tsx +++ b/packages/main/cypress/specs/Calendar.cy.tsx @@ -408,9 +408,19 @@ describe("Calendar general interaction", () => { .find("[ui5-daypicker]") .shadow() .find("[tabindex='0']") - .realClick(); + .realClick() + .should("have.focus"); cy.focused().realPress(["Shift", "F4"]); + + // Wait for focus to settle before proceeding + cy.get("#calendar1") + .shadow() + .find("[ui5-yearpicker]") + .shadow() + .find("[tabindex='0']") + .should("have.focus"); + cy.focused().realPress("PageUp"); cy.get("#calendar1") @@ -419,6 +429,14 @@ describe("Calendar general interaction", () => { expect(new Date(_timestamp * 1000)).to.deep.equal(new Date(Date.UTC(1980, 9, 1, 0, 0, 0))); }); + // Wait for focus to settle before proceeding + cy.get("#calendar1") + .shadow() + .find("[ui5-yearpicker]") + .shadow() + .find("[tabindex='0']") + .should("have.focus"); + cy.focused().realPress("PageDown"); cy.get("#calendar1") @@ -441,6 +459,14 @@ describe("Calendar general interaction", () => { expect(new Date(_timestamp * 1000)).to.deep.equal(new Date(Date.UTC(1998, 9, 16, 0, 0, 0))); }); + // Wait for focus to settle before proceeding + cy.get("#calendar1") + .shadow() + .find("[ui5-yearrangepicker]") + .shadow() + .find("[tabindex='0']") + .should("have.focus"); + cy.focused().realPress("PageUp"); cy.get("#calendar1") @@ -463,6 +489,14 @@ describe("Calendar general interaction", () => { expect(new Date(_timestamp * 1000)).to.deep.equal(new Date(Date.UTC(1998, 9, 16, 0, 0, 0))); }); + // Wait for focus to settle before proceeding + cy.get("#calendar1") + .shadow() + .find("[ui5-yearrangepicker]") + .shadow() + .find("[tabindex='0']") + .should("have.focus"); + cy.focused().realPress("PageDown"); cy.get("#calendar1") @@ -503,7 +537,8 @@ describe("Calendar general interaction", () => { cy.get("#calendar1").invoke("prop", "timestamp", timestamp); cy.ui5CalendarGetDay("#calendar1", timestamp.toString()) - .focus(); + .focus() + .should("have.focus"); // Select the focused date cy.focused().realPress("Space"); @@ -1254,3 +1289,148 @@ describe("Calendar accessibility", () => { }); }); }); + +describe("Day Picker Tests", () => { + it("Select day with Space", () => { + cy.mount(); + + cy.get("#calendar1") + .shadow() + .find("[ui5-daypicker]") + .shadow() + .find(".ui5-dp-item--now") + .as("today"); + + cy.get("@today") + .realClick() + .should("be.focused") + .realPress("ArrowRight") + .realPress("Space"); + + cy.focused() + .invoke("attr", "data-sap-timestamp") + .then(timestampAttr => { + const timestamp = parseInt(timestampAttr!); + const selectedDate = new Date(timestamp * 1000).getDate(); + const expectedDate = new Date(Date.now() + 24 * 3600 * 1000).getDate(); + expect(selectedDate).to.eq(expectedDate); + }); + + cy.get("#calendar1") + .should(($calendar) => { + const selectedDates = $calendar.prop("selectedDates"); + expect(selectedDates).to.have.length.greaterThan(0); + }); + }); + + it("Select day with Enter", () => { + const today = new Date(); + const tomorrow = Math.floor(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate() + 1, 0, 0, 0, 0) / 1000); + + cy.mount(); + + cy.get("#calendar1") + .shadow() + .find("[ui5-daypicker]") + .shadow() + .find(".ui5-dp-item--now") + .realClick(); + + // Wait for focus to settle before proceeding + cy.get("#calendar1") + .shadow() + .find("[ui5-daypicker]") + .shadow() + .find("[tabindex='0']") + .should("have.focus"); + + cy.get("#calendar1") + .realPress("ArrowRight"); + + cy.get("#calendar1") + .shadow() + .find("[ui5-daypicker]") + .shadow() + .find(`[data-sap-timestamp='${tomorrow}']`) + .should("have.focus"); + + cy.get("#calendar1") + .realPress("Enter"); + + // assert the date after today is selected + cy.get("#calendar1") + .should(($calendar) => { + const selectedDates = $calendar.prop("selectedDates"); + expect(selectedDates).to.include(tomorrow); + }); + }); + + it("Day names are correctly displayed", () => { + cy.mount(); + + cy.get("#calendar1") + .shadow() + .find("[ui5-daypicker]") + .shadow() + .find(".ui5-dp-firstday") + .first() + .should("have.text", "Sun"); // English default + }); + + it("Day names container has proper structure", () => { + cy.mount(); + + cy.get("#calendar1") + .shadow() + .find("[ui5-daypicker]") + .shadow() + .find(".ui5-dp-days-names-container") + .should("exist") + .find("[role='columnheader']") + .should("have.length", 8); + }); + + it("Arrow navigation works in day picker", () => { + const date = new Date(Date.UTC(2000, 10, 15, 0, 0, 0)); + cy.mount(getDefaultCalendar(date)); + + const timestamp = new Date(Date.UTC(2000, 10, 15, 0, 0, 0)).valueOf() / 1000; + const nextDayTimestamp = new Date(Date.UTC(2000, 10, 16, 0, 0, 0)).valueOf() / 1000; + + cy.ui5CalendarGetDay("#calendar1", timestamp.toString()) + .realClick() + .should("have.focus"); + + cy.focused().realPress("ArrowRight"); + + cy.ui5CalendarGetDay("#calendar1", nextDayTimestamp.toString()) + .should("have.focus"); + + cy.focused().realPress("ArrowLeft"); + + cy.ui5CalendarGetDay("#calendar1", timestamp.toString()) + .should("have.focus"); + }); + + it("Today's date is highlighted correctly", () => { + cy.mount(); + + cy.get("#calendar1") + .shadow() + .find("[ui5-daypicker]") + .shadow() + .find(".ui5-dp-item--now") + .should("exist") + .and("be.visible") + .invoke("attr", "data-sap-timestamp") + .then(timestampAttr => { + const timestamp = parseInt(timestampAttr!); + const todayFromTimestamp = new Date(timestamp * 1000); + const actualToday = new Date(); + + expect(todayFromTimestamp.getDate()).to.equal(actualToday.getDate()); + expect(todayFromTimestamp.getMonth()).to.equal(actualToday.getMonth()); + expect(todayFromTimestamp.getFullYear()).to.equal(actualToday.getFullYear()); + }); + }); +}); diff --git a/packages/main/cypress/specs/DatePicker.cy.tsx b/packages/main/cypress/specs/DatePicker.cy.tsx index 63981b62b647..0f7fbb3866a3 100644 --- a/packages/main/cypress/specs/DatePicker.cy.tsx +++ b/packages/main/cypress/specs/DatePicker.cy.tsx @@ -511,12 +511,35 @@ describe("Date Picker Tests", () => { .as("datePicker") .ui5DatePickerValueHelpIconPress(); + // Focus the day picker's focusable element first cy.get("@datePicker") .shadow() .find("ui5-calendar") .as("calendar") - .realPress(["Shift", "F4"]) - .realPress("F4"); + .shadow() + .find("ui5-daypicker") + .shadow() + .find("[tabindex='0']") + .should("be.visible") + .focus() + .should("have.focus"); + + cy.focused().realPress(["Shift", "F4"]); + + // Wait for year picker to be visible and focused + cy.get("@calendar") + .shadow() + .find("ui5-yearpicker") + .should("be.visible"); + + cy.get("@calendar") + .shadow() + .find("ui5-yearpicker") + .shadow() + .find("[tabindex='0']") + .should("have.focus"); + + cy.focused().realPress("F4"); cy.get("@calendar") .shadow() @@ -531,12 +554,35 @@ describe("Date Picker Tests", () => { .as("datePicker") .ui5DatePickerValueHelpIconPress(); + // Focus the day picker's focusable element first cy.get("@datePicker") .shadow() .find("ui5-calendar") .as("calendar") - .realPress("F4") - .realPress(["Shift", "F4"]); + .shadow() + .find("ui5-daypicker") + .shadow() + .find("[tabindex='0']") + .should("be.visible") + .focus() + .should("have.focus"); + + cy.focused().realPress("F4"); + + // Wait for month picker to be visible and focused + cy.get("@calendar") + .shadow() + .find("ui5-monthpicker") + .should("be.visible"); + + cy.get("@calendar") + .shadow() + .find("ui5-monthpicker") + .shadow() + .find("[tabindex='0']") + .should("have.focus"); + + cy.focused().realPress(["Shift", "F4"]); cy.get("@calendar") .shadow() diff --git a/packages/main/cypress/specs/DateRangePicker.cy.tsx b/packages/main/cypress/specs/DateRangePicker.cy.tsx index 1957c15496c4..21190e781949 100644 --- a/packages/main/cypress/specs/DateRangePicker.cy.tsx +++ b/packages/main/cypress/specs/DateRangePicker.cy.tsx @@ -1,4 +1,4 @@ -import "../../dist/Assets.js"; +import "../../src/Assets.js"; import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; import DateRangePicker from "../../src/DateRangePicker.js"; import Label from "../../src/Label.js"; diff --git a/packages/main/cypress/specs/DayPicker.cy.tsx b/packages/main/cypress/specs/DayPicker.cy.tsx deleted file mode 100644 index 59cf1e56373e..000000000000 --- a/packages/main/cypress/specs/DayPicker.cy.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import "../../src/Assets.js"; -import DayPicker from "../../src/DayPicker.js"; -import { setLanguage } from "@ui5/webcomponents-base/dist/config/Language.js"; -import "@ui5/webcomponents-localization/dist/features/calendar/Gregorian.js"; - -function DefaultDayPicker() { - return ; -} - -describe("Day Picker Tests", () => { - it("Select day with Space", () => { - cy.mount(); - cy.get("#daypicker") - .shadow() - .find(".ui5-dp-item--now") - .as("today"); - cy.get("@today") - .realClick() - .should("be.focused") - .realPress("ArrowRight") - .realPress("Space"); - cy.focused() - .invoke("attr", "data-sap-timestamp") - .then(timestampAttr => { - const timestamp = parseInt(timestampAttr!); - const selectedDate = new Date(timestamp * 1000).getDate(); - const expectedDate = new Date(Date.now() + 24 * 3600 * 1000).getDate(); - expect(selectedDate).to.eq(expectedDate); - }); - }); - - it("Select day with Enter", () => { - cy.mount(); - cy.get("#daypicker") - .shadow() - .find(".ui5-dp-item--now") - .realClick() - .should("be.focused") - .as("today"); - - cy.get("@today") - .should("be.focused") - .realPress("ArrowRight") - .realPress("Enter"); - - cy.focused() - .invoke("attr", "data-sap-timestamp") - .then(timestampAttr => { - const timestamp = parseInt(timestampAttr!); - const selectedDate = new Date(timestamp * 1000).getDate(); - const expectedDate = new Date(Date.now() + 24 * 3600 * 1000).getDate(); - expect(selectedDate).to.eq(expectedDate); - }); - }); - - it("Day names are correctly displayed when length is less than 3", () => { - cy.wrap({ setLanguage }) - .invoke("setLanguage", "en"); - - cy.mount(); - cy.get("#daypicker") - .shadow() - .find(".ui5-dp-firstday") - .first() - .should("have.text", "Sun"); - }); - - it("Day names are correctly displayed when length is more than 3", () => { - // Set configuration first - cy.wrap({ setLanguage }) - .invoke("setLanguage", "pt_PT"); - - cy.mount(); - - cy.get("#daypicker") - .shadow() - .find(".ui5-dp-firstday") - .first() - .should("have.text", "D"); - }); -}); diff --git a/packages/main/src/Calendar.ts b/packages/main/src/Calendar.ts index 75fbb4a26221..ba14a1a2db4c 100644 --- a/packages/main/src/Calendar.ts +++ b/packages/main/src/Calendar.ts @@ -56,12 +56,11 @@ import { } from "./generated/i18n/i18n-defaults.js"; import type { YearRangePickerChangeEventDetail } from "./YearRangePicker.js"; -interface ICalendarPicker { +interface ICalendarPicker extends HTMLElement { _showPreviousPage: () => void, _showNextPage: () => void, _hasPreviousPage: () => boolean, _hasNextPage: () => boolean, - _autoFocus?: boolean, _currentYearRange?: CalendarYearRangeT, } @@ -474,7 +473,6 @@ class Calendar extends CalendarPart { if (defaultTypes.includes(this._selectedItemType)) { this._selectedItemType = "None"; // In order to avoid filtering of default types } - this._currentPickerDOM._autoFocus = false; } /** @@ -482,11 +480,11 @@ class Calendar extends CalendarPart { */ _normalizeCurrentPicker() { if (this._currentPicker === "day" && this._pickersMode !== CalendarPickersMode.DAY_MONTH_YEAR) { - this._currentPicker = "month"; + this.switchToMonthPicker(true); } if (this._currentPicker === "month" && this._pickersMode === CalendarPickersMode.YEAR) { - this._currentPicker = "year"; + this.switchToYearPicker(true); } } @@ -528,40 +526,57 @@ class Calendar extends CalendarPart { /** * The user clicked the "month" button in the header */ - onHeaderShowMonthPress() { - this.showMonth(); + onHeaderMonthButtonPress() { + this.switchToMonthPicker(); this.fireDecoratorEvent("show-month-view"); } - showMonth() { - this._currentPickerDOM._autoFocus = false; + async switchToDayPicker(suppressFocus = false) { + this._currentPicker = "day"; + if (!suppressFocus) { + await renderFinished(); + this._currentPickerDOM.focus(); + } + } + + async switchToMonthPicker(suppressFocus = false) { this._currentPicker = "month"; + if (!suppressFocus) { + await renderFinished(); + this._currentPickerDOM.focus(); + } } /** * The user clicked the "year" button in the header */ - onHeaderShowYearPress() { - this.showYear(); + onHeaderYearButtonPress() { + this.switchToYearPicker(); this.fireDecoratorEvent("show-year-view"); } - showYear() { - this._currentPickerDOM._autoFocus = false; + async switchToYearPicker(suppressFocus = false) { this._currentPicker = "year"; + if (!suppressFocus) { + await renderFinished(); + this._currentPickerDOM.focus(); + } } /** * The user clicked the "year range" button in the YearPicker header */ - onHeaderShowYearRangePress() { - this.showYearRange(); + onHeaderYearRangeButtonPress() { + this.switchToYearRangePicker(); this.fireDecoratorEvent("show-year-range-view"); } - showYearRange() { - this._currentPickerDOM._autoFocus = false; + async switchToYearRangePicker(suppressFocus = false) { this._currentPicker = "yearrange"; + if (!suppressFocus) { + await renderFinished(); + this._currentPickerDOM.focus(); + } } get _currentPickerDOM() { @@ -574,10 +589,6 @@ class Calendar extends CalendarPart { */ onHeaderPreviousPress() { this._currentPickerDOM._showPreviousPage(); - - if (this.calendarLegend) { - this._currentPickerDOM._autoFocus = true; - } } /** @@ -585,10 +596,6 @@ class Calendar extends CalendarPart { */ onHeaderNextPress() { this._currentPickerDOM._showNextPage(); - - if (this.calendarLegend) { - this._currentPickerDOM._autoFocus = true; - } } _setSecondaryCalendarTypeButtonText() { @@ -715,41 +722,38 @@ class Calendar extends CalendarPart { this.timestamp = e.detail.timestamp; if (this._pickersMode === CalendarPickersMode.DAY_MONTH_YEAR) { - this._currentPicker = "day"; + this.switchToDayPicker(); } else { this._fireEventAndUpdateSelectedDates(e.detail.dates); } - - this._currentPickerDOM._autoFocus = true; } onSelectedYearChange(e: CustomEvent) { this.timestamp = e.detail.timestamp; if (this._pickersMode === CalendarPickersMode.DAY_MONTH_YEAR) { - this._currentPicker = "day"; + this.switchToDayPicker(); } else if (this._pickersMode === CalendarPickersMode.MONTH_YEAR) { - this._currentPicker = "month"; + this.switchToMonthPicker(); } else { this._fireEventAndUpdateSelectedDates(e.detail.dates); } - - this._currentPickerDOM._autoFocus = true; } onSelectedYearRangeChange(e: CustomEvent) { this.timestamp = e.detail.timestamp; - this._currentPicker = "year"; - this._currentPickerDOM._autoFocus = true; + this.switchToYearPicker(); } - onNavigate(e: CustomEvent) { + async onNavigate(e: CustomEvent) { this.timestamp = e.detail.timestamp; + await renderFinished(); + this._currentPickerDOM.focus(); } _onkeydown(e: KeyboardEvent) { if (isF4(e) && this._currentPicker !== "month") { - this._currentPicker = "month"; + this.switchToMonthPicker(); this.fireDecoratorEvent("show-month-view"); } @@ -758,10 +762,10 @@ class Calendar extends CalendarPart { } if (this._currentPicker !== "year") { - this._currentPicker = "year"; + this.switchToYearPicker(); this.fireDecoratorEvent("show-year-view"); } else { - this._currentPicker = "yearrange"; + this.switchToYearRangePicker(); this.fireDecoratorEvent("show-year-range-view"); } } @@ -861,7 +865,7 @@ class Calendar extends CalendarPart { } if (isEnter(e)) { - this.showMonth(); + this.switchToMonthPicker(); this.fireDecoratorEvent("show-month-view"); } } @@ -869,7 +873,7 @@ class Calendar extends CalendarPart { onMonthButtonKeyUp(e: KeyboardEvent) { if (isSpace(e)) { e.preventDefault(); - this.showMonth(); + this.switchToMonthPicker(); this.fireDecoratorEvent("show-month-view"); } } @@ -880,14 +884,14 @@ class Calendar extends CalendarPart { } if (isEnter(e)) { - this.showYear(); + this.switchToYearPicker(); this.fireDecoratorEvent("show-year-view"); } } onYearButtonKeyUp(e: KeyboardEvent) { if (isSpace(e)) { - this.showYear(); + this.switchToYearPicker(); this.fireDecoratorEvent("show-year-view"); } } @@ -898,14 +902,14 @@ class Calendar extends CalendarPart { } if (isEnter(e)) { - this.showYearRange(); + this.switchToYearRangePicker(); this.fireDecoratorEvent("show-year-range-view"); } } onYearRangeButtonKeyUp(e: KeyboardEvent) { if (isSpace(e)) { - this.showYearRange(); + this.switchToYearRangePicker(); this.fireDecoratorEvent("show-year-range-view"); } } diff --git a/packages/main/src/CalendarHeaderTemplate.tsx b/packages/main/src/CalendarHeaderTemplate.tsx index bb8a039ffe44..c3193e95c41d 100644 --- a/packages/main/src/CalendarHeaderTemplate.tsx +++ b/packages/main/src/CalendarHeaderTemplate.tsx @@ -32,7 +32,7 @@ export default function CalendarTemplate(this: Calendar) { aria-description={this.accInfo.ariaLabelMonthButton} title={this.accInfo.tooltipMonthButton} aria-keyshortcuts={this.accInfo.keyShortcutMonthButton} - onClick={this.onHeaderShowMonthPress} + onClick={this.onHeaderMonthButtonPress} onKeyDown={this.onMonthButtonKeyDown} onKeyUp={this.onMonthButtonKeyUp} > @@ -51,7 +51,7 @@ export default function CalendarTemplate(this: Calendar) { role="button" aria-label={this.accInfo.ariaLabelYearButton} aria-description={this.accInfo.ariaLabelYearButton} - onClick={this.onHeaderShowYearPress} + onClick={this.onHeaderYearButtonPress} onKeyDown={this.onYearButtonKeyDown} onKeyUp={this.onYearButtonKeyUp} title={this.accInfo.tooltipYearButton} @@ -73,7 +73,7 @@ export default function CalendarTemplate(this: Calendar) { aria-description={this.accInfo.ariaLabelYearRangeButton} title={this.accInfo.tooltipYearRangeButton} aria-keyshortcuts={this.accInfo.keyShortcutYearRangeButton} - onClick={this.onHeaderShowYearRangePress} + onClick={this.onHeaderYearRangeButtonPress} onKeyDown={this.onYearRangeButtonKeyDown} onKeyUp={this.onYearRangeButtonKeyUp} > diff --git a/packages/main/src/DayPickerTemplate.tsx b/packages/main/src/DayPickerTemplate.tsx index 6e9735d12493..b20681b6ff41 100644 --- a/packages/main/src/DayPickerTemplate.tsx +++ b/packages/main/src/DayPickerTemplate.tsx @@ -15,8 +15,6 @@ export default function DayPickerTemplate(this: DayPicker) { onKeyUp={this._onkeyup} onClick={this._onclick} onMouseOver={this._onmouseover} - onFocusIn={this._onfocusin} - onFocusOut={this._onfocusout} >
diff --git a/packages/main/src/MonthPicker.ts b/packages/main/src/MonthPicker.ts index 4c4065ad0a29..812f4540badd 100644 --- a/packages/main/src/MonthPicker.ts +++ b/packages/main/src/MonthPicker.ts @@ -143,12 +143,6 @@ class MonthPicker extends CalendarPart implements ICalendarPicker { this._buildMonths(); } - onAfterRendering() { - if (!this._hidden) { - this.focus(); - } - } - get rowSize() { return (this.secondaryCalendarType === CalendarType.Islamic && this.primaryCalendarType !== CalendarType.Islamic) || (this.secondaryCalendarType === CalendarType.Persian && this.primaryCalendarType !== CalendarType.Persian) ? 2 : 3; @@ -330,6 +324,21 @@ class MonthPicker extends CalendarPart implements ICalendarPicker { } } + /** + * Sets the focus reference to the month that was clicked with mousedown. + * @param e + * @private + */ + _onmousedown(e: MouseEvent) { + const target = e.target as HTMLElement; + const clickedItem = target.closest(".ui5-mp-item") as HTMLElement; + + if (clickedItem) { + const timestamp = this._getTimestampFromDom(clickedItem); + this._setTimestamp(timestamp); + } + } + /** * Modifies timestamp by a given amount of months and, * if necessary, loads the prev/next page. diff --git a/packages/main/src/MonthPickerTemplate.tsx b/packages/main/src/MonthPickerTemplate.tsx index d4a3a95e16af..51e78f5aa0d0 100644 --- a/packages/main/src/MonthPickerTemplate.tsx +++ b/packages/main/src/MonthPickerTemplate.tsx @@ -13,6 +13,7 @@ export default function MonthPickerTemplate(this: MonthPicker) { onKeyDown={this._onkeydown} onKeyUp={this._onkeyup} onClick={this._selectMonth} + onMouseDown={this._onmousedown} > {this._monthsInterval.map(months =>
diff --git a/packages/main/src/YearPicker.ts b/packages/main/src/YearPicker.ts index d359cd24f711..e837ae2e7232 100644 --- a/packages/main/src/YearPicker.ts +++ b/packages/main/src/YearPicker.ts @@ -240,12 +240,6 @@ class YearPicker extends CalendarPart implements ICalendarPicker { this._yearsInterval = intervals; } - onAfterRendering() { - if (!this._hidden) { - this.focus(); - } - } - /** * Returns true if year timestamp is inside the selection range. * @private diff --git a/packages/main/src/YearRangePicker.ts b/packages/main/src/YearRangePicker.ts index 8802ae52eac6..07b6086cca6e 100644 --- a/packages/main/src/YearRangePicker.ts +++ b/packages/main/src/YearRangePicker.ts @@ -313,12 +313,6 @@ class YearRangePicker extends CalendarPart implements ICalendarPicker { return isBetweenInclusive(timestamp, this.selectedDates[0], this.selectedDates[1]); } - onAfterRendering() { - if (!this._hidden) { - this.focus(); - } - } - _onkeydown(e: KeyboardEvent) { let preventDefault = true; const pageSize = this._getPageSize();