From d044f81d5c818eb28b09518e76b92e4a94b4394a Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Sun, 28 Jun 2026 03:23:30 +0300 Subject: [PATCH 1/7] fix(ui5-file-uploader): announce value state message to screen readers Issue: - The value state message of ui5-file-uploader was not announced by screen readers (NVDA, JAWS) when the component received focus. - Unlike ui5-input, the native input type file had no aria-describedby link to the value state text, so AT had no way to discover the message regardless of whether the visual popover was open. Solution: - Added an ariaValueStateHiddenText getter that composes the full AT announcement string from the value state type label and the message text, mirroring the existing pattern in ui5-input. - Added ariaDescribedBy to accInfo, pointing to valueStateDesc when a value state is active. - Added a hidden span with id valueStateDesc in FileUploaderTemplate within the same shadow root as the native input, so the aria-describedby reference resolves correctly across renders. - The hidden span is intentionally placed in FileUploaderTemplate and not in FileUploaderPopoverTemplate, as aria-describedby cannot cross shadow DOM boundaries into the popover's own shadow root. Fixed: https://github.com/UI5/webcomponents/issues/13549 --- packages/main/src/FileUploader.ts | 30 ++++++++++++++++++++++ packages/main/src/FileUploaderTemplate.tsx | 4 +++ 2 files changed, 34 insertions(+) diff --git a/packages/main/src/FileUploader.ts b/packages/main/src/FileUploader.ts index 1c55c49063d3..5a8d4aad18b6 100644 --- a/packages/main/src/FileUploader.ts +++ b/packages/main/src/FileUploader.ts @@ -35,6 +35,10 @@ import { VALUE_STATE_INFORMATION, VALUE_STATE_ERROR, VALUE_STATE_WARNING, + VALUE_STATE_TYPE_SUCCESS, + VALUE_STATE_TYPE_INFORMATION, + VALUE_STATE_TYPE_ERROR, + VALUE_STATE_TYPE_WARNING, FILEUPLOADER_DEFAULT_PLACEHOLDER, FILEUPLOADER_DEFAULT_MULTIPLE_PLACEHOLDER, FILEUPLOADER_ROLE_DESCRIPTION, @@ -624,9 +628,35 @@ class FileUploader extends UI5Element implements IFormInputElement { "ariaHasPopup": "dialog", "ariaLabel": getAllAccessibleNameRefTexts(this) || getEffectiveAriaLabelText(this) || getAssociatedLabelForTexts(this) || undefined, "ariaDescription": getAllAccessibleDescriptionRefTexts(this) || getEffectiveAriaDescriptionText(this) || undefined, + "ariaDescribedBy": this.hasValueState ? "valueStateDesc" : undefined, }; } + get valueStateTypeMappings(): Record { + return { + "Positive": FileUploader.i18nBundle.getText(VALUE_STATE_TYPE_SUCCESS), + "Information": FileUploader.i18nBundle.getText(VALUE_STATE_TYPE_INFORMATION), + "Negative": FileUploader.i18nBundle.getText(VALUE_STATE_TYPE_ERROR), + "Critical": FileUploader.i18nBundle.getText(VALUE_STATE_TYPE_WARNING), + }; + } + + get ariaValueStateHiddenText(): string | undefined { + if (!this.hasValueState) { + return undefined; + } + + const valueStateType = this.valueStateTypeMappings[this.valueState]; + + if (this.shouldDisplayDefaultValueStateMessage) { + return this.valueStateText ? `${valueStateType} ${this.valueStateText}` : valueStateType; + } + + return this.valueStateMessage.length + ? `${valueStateType} ${this.valueStateMessage.map(el => el.textContent).join(" ")}` + : valueStateType; + } + get inputTitle(): string { return FileUploader.i18nBundle.getText(FILEUPLOADER_INPUT_TOOLTIP); } diff --git a/packages/main/src/FileUploaderTemplate.tsx b/packages/main/src/FileUploaderTemplate.tsx index bbceeaa01d3d..a54954d0ca88 100644 --- a/packages/main/src/FileUploaderTemplate.tsx +++ b/packages/main/src/FileUploaderTemplate.tsx @@ -35,10 +35,14 @@ export default function FileUploaderTemplate(this: FileUploader) { aria-description={this.accInfo.ariaDescription} aria-required={this.accInfo.ariaRequired} aria-invalid={this.accInfo.ariaInvalid} + aria-describedby={this.accInfo.ariaDescribedBy} onClick={this._onNativeInputClick} onChange={this._onChange} data-sap-focus-ref /> + {this.hasValueState && + {this.ariaValueStateHiddenText} + } {this.hideInput ? ( From 4b9ae7ed8ec60cccada4bc124a4c4cbfd1d0a582 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Sun, 28 Jun 2026 03:29:03 +0300 Subject: [PATCH 2/7] fix(ui5-file-uploader): announce value state message to screen readers --- .../main/cypress/specs/FileUploader.cy.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/main/cypress/specs/FileUploader.cy.tsx b/packages/main/cypress/specs/FileUploader.cy.tsx index b2537c4decec..280b0611b7d0 100644 --- a/packages/main/cypress/specs/FileUploader.cy.tsx +++ b/packages/main/cypress/specs/FileUploader.cy.tsx @@ -478,6 +478,31 @@ describe("Interaction", () => { }); describe("Accessibility", () => { + it("value state hidden text contains type label and default message", () => { + cy.mount( + + ); + + cy.get("#uploader") + .shadow() + .find("#valueStateDesc") + .should("have.text", "Value State Error Invalid entry"); + }); + + it("value state hidden text contains type label and custom message", () => { + cy.mount( + +
Custom error message.
+
+ ); + + cy.get("#uploader") + .shadow() + .find("#valueStateDesc") + .should("include.text", "Value State Error") + .and("include.text", "Custom error message."); + }); + it("A11y attributes", () => { cy.mount( <> From 0667895b20a5be5c93b036acb4b973b446608ea3 Mon Sep 17 00:00:00 2001 From: Boyan Rakilovski Date: Thu, 9 Jul 2026 09:56:56 +0300 Subject: [PATCH 3/7] fix(ui5-file-uploader): announce value state message to screen readers --- packages/main/src/FileUploader.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/main/src/FileUploader.ts b/packages/main/src/FileUploader.ts index 5a8d4aad18b6..9f8f14218861 100644 --- a/packages/main/src/FileUploader.ts +++ b/packages/main/src/FileUploader.ts @@ -59,6 +59,8 @@ import ValueStateMessageCss from "./generated/themes/ValueStateMessage.css.js"; const convertBytesToMegabytes = (bytes: number) => (bytes / 1024) / 1024; +type MappedValueState = Exclude<`${ValueState}`, "None">; + type FileData = { fileName: string, fileSize: number, @@ -632,7 +634,7 @@ class FileUploader extends UI5Element implements IFormInputElement { }; } - get valueStateTypeMappings(): Record { + get valueStateTypeMappings(): Record { return { "Positive": FileUploader.i18nBundle.getText(VALUE_STATE_TYPE_SUCCESS), "Information": FileUploader.i18nBundle.getText(VALUE_STATE_TYPE_INFORMATION), @@ -646,7 +648,7 @@ class FileUploader extends UI5Element implements IFormInputElement { return undefined; } - const valueStateType = this.valueStateTypeMappings[this.valueState]; + const valueStateType = this.valueStateTypeMappings[this.valueState as MappedValueState]; if (this.shouldDisplayDefaultValueStateMessage) { return this.valueStateText ? `${valueStateType} ${this.valueStateText}` : valueStateType; @@ -675,7 +677,7 @@ class FileUploader extends UI5Element implements IFormInputElement { return this.placeholder ?? (this.multiple ? multiplePlaceholder : singlePlaceholder); } - get valueStateTextMappings(): Record { + get valueStateTextMappings(): Record { return { "Positive": FileUploader.i18nBundle.getText(VALUE_STATE_SUCCESS), "Information": FileUploader.i18nBundle.getText(VALUE_STATE_INFORMATION), @@ -684,8 +686,8 @@ class FileUploader extends UI5Element implements IFormInputElement { }; } - get valueStateText(): string { - return this.valueStateTextMappings[this.valueState]; + get valueStateText(): string | undefined { + return this.valueStateTextMappings[this.valueState as MappedValueState]; } get hasValueState(): boolean { From c93f2b7e732bf11685f6a3479fe9815a48d42bdc Mon Sep 17 00:00:00 2001 From: Diana Date: Tue, 7 Jul 2026 16:22:11 +0300 Subject: [PATCH 4/7] fix(ui5-calendar): select correct day in other month (#13786) Issue: Clicking a date from the prev/next month would select the wrong date. On mousedown, the calendar navigates to the prev/next month and re-renders before the click event fires. The click handler then reads the outdated data-sap-timestamp (whatever date that's currenlty at that grid position in the new month), and selects that instead of the originally clicked date. Solution: Use the timestamp already set by _onmousedown instead of re-reading from the DOM on mouse clicks, and restore focus after the re-render when an other-month date was clicked. Fixes: #13760 --- packages/main/cypress/specs/Calendar.cy.tsx | 18 ++++++++++++++++++ packages/main/src/DayPicker.ts | 16 ++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/main/cypress/specs/Calendar.cy.tsx b/packages/main/cypress/specs/Calendar.cy.tsx index 9ed1429585ac..cabc564f7a6f 100644 --- a/packages/main/cypress/specs/Calendar.cy.tsx +++ b/packages/main/cypress/specs/Calendar.cy.tsx @@ -1761,6 +1761,24 @@ describe("Day Picker Tests", () => { }); }); + it("clicking on a day in the next month selects that day and moves focus to it", () => { + // February 2026: the last row shows March 1 - 14 as other-month days + const date = new Date(Date.UTC(2026, 1, 14, 0, 0, 0)); + cy.mount(getDefaultCalendar(date)); + + const march1Timestamp = new Date(Date.UTC(2026, 2, 1, 0, 0, 0)).valueOf() / 1000; + + cy.ui5CalendarGetDay("#calendar1", march1Timestamp.toString()) + .realClick(); + + // Calendar navigates to March; March 1 must be selected and focused + cy.ui5CalendarGetDay("#calendar1", march1Timestamp.toString()) + .should("have.class", "ui5-dp-item--selected"); + + cy.ui5CalendarGetDay("#calendar1", march1Timestamp.toString()) + .should("be.focused"); + }); + it("mousedown + arrow navigation + click keeps focus at navigated cell, selection on clicked cell", () => { const date = new Date(Date.UTC(2000, 9, 10, 0, 0, 0)); cy.mount(getDefaultCalendar(date)); diff --git a/packages/main/src/DayPicker.ts b/packages/main/src/DayPicker.ts index 25a793bd42a1..b145a3b57920 100644 --- a/packages/main/src/DayPicker.ts +++ b/packages/main/src/DayPicker.ts @@ -210,6 +210,7 @@ class DayPicker extends CalendarPart implements ICalendarPicker { _focusableDay!: HTMLElement; _autoFocus?: boolean; + _mousedownTimestamp?: number; @i18n("@ui5/webcomponents") static i18nBundle: I18nBundle; @@ -518,7 +519,8 @@ class DayPicker extends CalendarPart implements ICalendarPicker { return; } - const timestamp = this._getTimestampFromDom(target); + const timestamp = setTimestamp ? this._getTimestampFromDom(target) : (this._mousedownTimestamp ?? this.timestamp!); + this._mousedownTimestamp = undefined; if (setTimestamp) { this._safelySetTimestamp(timestamp); @@ -612,7 +614,17 @@ class DayPicker extends CalendarPart implements ICalendarPicker { return; } - this._safelySetTimestamp(this._getTimestampFromDom(target)); + const timestamp = this._getTimestampFromDom(target); + const clickedDate = CalendarDate.fromTimestamp(timestamp * 1000, this._primaryCalendarType); + const isOtherMonth = clickedDate.getMonth() !== this._calendarDate.getMonth(); + + this._mousedownTimestamp = timestamp; + this._safelySetTimestamp(timestamp); + + if (isOtherMonth) { + this._autoFocus = true; + } + this.fireDecoratorEvent("navigate", { timestamp: this.timestamp!, mouse: true }); } From d4af87539f19a061432c118f3b5b4fb7a4f189e3 Mon Sep 17 00:00:00 2001 From: kskondov Date: Wed, 8 Jul 2026 11:13:27 +0300 Subject: [PATCH 5/7] feat(ui5-dialog): add fullscreen toggle button (#13691) * feat(ui5-dialog): add fullscreen toggle button The dialog now supports a fullscreen toggle button in the header, controlled by the `showFullscreenButton` property. The button toggles the `stretch` property and is not available on phone devices. - Keyboard shortcut: Shift+Ctrl+F - Double-click on header toggles fullscreen - Button shows Maximize/Restore tooltip and aria-keyshortcuts - Resets drag/resize state when toggling * feat(ui5-dialog): add fullscreen toggle button Adds a padding-inline-end to prevents header content from overlapping with the button * feat(ui5-dialog): add fullscreen toggle button Lint errors fix * feat(ui5-dialog): add fullscreen toggle button Lint and bug fixes - Keyboard shortcut: Shift+Ctrl+F (works regardless of focus) - Double-click on header toggles fullscreen - Button shows Maximize/Restore tooltip and aria-keyshortcuts - Resets drag/resize state when toggling * feat(ui5-dialog): add fullscreen toggle button Removes absolute positioning and and adds fullscreen spacing instead of offset - Keyboard shortcut: Shift+Ctrl+F (works regardless of focus) - Double-click on header toggles fullscreen - Button shows Maximize/Restore tooltip and aria-keyshortcuts - Resets drag/resize state when toggling * feat(ui5-dialog): add fullscreen toggle button Removed unnecessary spacing * feat(ui5-dialog): add fullscreen toggle button disable fullscreen button if custom header is provided - Keyboard shortcut: Shift+Ctrl+F (works regardless of focus) - Double-click on header toggles fullscreen - Button shows Maximize/Restore tooltip and aria-keyshortcuts - Resets drag/resize state when toggling * chore: resolve merge conflics * chore: fix double click and acc texts * chore: fix first focusable element * chore: fix tests * chore: fix lint error * chore: fix test * chore: fix toggling multiple dialogs * chore: address code comments * chore: address code comments and add tests * chore: add release version * chore: address code comments * chore: fix failing test * chore: fix failing test * chore: change since version --------- Co-authored-by: Teodor Taushanov --- packages/main/cypress/specs/Dialog.cy.tsx | 362 ++++++++++++++++-- packages/main/src/Dialog.ts | 127 +++++- packages/main/src/DialogTemplate.tsx | 13 + packages/main/src/Popup.ts | 10 +- .../main/src/i18n/messagebundle.properties | 7 + packages/main/src/themes/Dialog.css | 23 +- packages/main/test/pages/Dialog.html | 48 +++ .../docs/_components_pages/main/Dialog.mdx | 5 + .../main/Dialog/Fullscreen/Fullscreen.md | 5 + .../_samples/main/Dialog/Fullscreen/main.js | 21 + .../main/Dialog/Fullscreen/sample.html | 29 ++ .../main/Dialog/Fullscreen/sample.tsx | 50 +++ 12 files changed, 671 insertions(+), 29 deletions(-) create mode 100644 packages/website/docs/_samples/main/Dialog/Fullscreen/Fullscreen.md create mode 100644 packages/website/docs/_samples/main/Dialog/Fullscreen/main.js create mode 100644 packages/website/docs/_samples/main/Dialog/Fullscreen/sample.html create mode 100644 packages/website/docs/_samples/main/Dialog/Fullscreen/sample.tsx diff --git a/packages/main/cypress/specs/Dialog.cy.tsx b/packages/main/cypress/specs/Dialog.cy.tsx index 23737eb04ac4..1055c24584d5 100644 --- a/packages/main/cypress/specs/Dialog.cy.tsx +++ b/packages/main/cypress/specs/Dialog.cy.tsx @@ -724,7 +724,7 @@ describe("Dialog general interaction", () => { .shadow() .find(".ui5-popup-resize-handle") .realMouseDown() - .realMouseMove(800, 0) // Large movement to ensure we hit min width + .realMouseMove(420, 0) // Large movement to ensure we hit min width .realMouseUp(); cy.get("#rtl-min-width-dialog").then(dialogAtMinWidth => { @@ -739,7 +739,7 @@ describe("Dialog general interaction", () => { .shadow() .find(".ui5-popup-resize-handle") .realMouseDown() - .realMouseMove(150, 0) // Additional rightward movement beyond min width + .realMouseMove(350, 0) // Additional rightward movement beyond min width .realMouseUp(); cy.get("#rtl-min-width-dialog").then(dialogAfterExtraResize => { @@ -860,7 +860,7 @@ describe("Dialog general interaction", () => { expect(widthBeforeResizing).to.equal(widthAfterResizing); expect(heightBeforeResizing).not.to.equal(heightAfterResizing); - expect(topBeforeResizing).to.equal(topAfterResizing); + expect(topBeforeResizing).to.be.closeTo(topAfterResizing, 2); expect(leftBeforeResizing).not.to.equal(leftAfterResizing + 100); }); }); @@ -1655,8 +1655,8 @@ describe("Event Registration", () => { ); // Check that resize handler is not attached when dialog is closed - cy.get("#dialog-resize-event").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-resize-event").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._screenResizeHandlerAttached).to.be.undefined; }); @@ -1665,8 +1665,8 @@ describe("Event Registration", () => { cy.get("#dialog-resize-event").ui5DialogOpened(); // Check that resize handler is attached when dialog is open - cy.get("#dialog-resize-event").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-resize-event").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._screenResizeHandlerAttached).to.be.true; }); @@ -1674,8 +1674,8 @@ describe("Event Registration", () => { cy.get("#dialog-resize-event").invoke("prop", "open", false); // Check that resize handler is detached when dialog is closed - cy.get("#dialog-resize-event").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-resize-event").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._screenResizeHandlerAttached).to.be.false; }); }); @@ -1690,8 +1690,8 @@ describe("Event Registration", () => { ); // Check that dragstart handler is not registered when dialog is closed - cy.get("#dialog-dragstart-event").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-dragstart-event").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._dragHandlerRegistered).to.be.false; }); @@ -1700,8 +1700,8 @@ describe("Event Registration", () => { cy.get("#dialog-dragstart-event").ui5DialogOpened(); // Check that dragstart handler is registered when dialog is open - cy.get("#dialog-dragstart-event").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-dragstart-event").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._dragHandlerRegistered).to.be.true; }); @@ -1709,8 +1709,8 @@ describe("Event Registration", () => { cy.get("#dialog-dragstart-event").invoke("prop", "open", false); // Check that dragstart handler is deregistered when dialog is closed - cy.get("#dialog-dragstart-event").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-dragstart-event").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._dragHandlerRegistered).to.be.false; }); }); @@ -1729,8 +1729,8 @@ describe("Event Registration", () => { cy.get("#dialog-reopen-events").ui5DialogOpened(); // Verify handlers are registered - cy.get("#dialog-reopen-events").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-reopen-events").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._screenResizeHandlerAttached).to.be.true; expect(dialog._dragHandlerRegistered).to.be.true; }); @@ -1739,8 +1739,8 @@ describe("Event Registration", () => { cy.get("#dialog-reopen-events").invoke("prop", "open", false); // Verify handlers are deregistered - cy.get("#dialog-reopen-events").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-reopen-events").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._screenResizeHandlerAttached).to.be.false; expect(dialog._dragHandlerRegistered).to.be.false; }); @@ -1750,8 +1750,8 @@ describe("Event Registration", () => { cy.get("#dialog-reopen-events").ui5DialogOpened(); // Verify handlers are registered again - cy.get("#dialog-reopen-events").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#dialog-reopen-events").then($dialog => { + const dialog = $dialog.get(0); expect(dialog._screenResizeHandlerAttached).to.be.true; expect(dialog._dragHandlerRegistered).to.be.true; }); @@ -1769,8 +1769,8 @@ describe("Native drag-and-drop in draggable dialogs", () => { cy.get("#test-dialog").invoke("prop", "open", true); cy.get("#test-dialog").ui5DialogOpened(); - cy.get("#test-dialog").then($dialog => { - const dialog = $dialog.get(0) as Dialog; + cy.get("#test-dialog").then($dialog => { + const dialog = $dialog.get(0); const content = document.getElementById("content-item"); // Create a mock event @@ -1831,9 +1831,9 @@ describe("Native drag-and-drop in draggable dialogs", () => { cy.get("#test-dialog").invoke("prop", "open", true); cy.get("#test-dialog").ui5DialogOpened(); - cy.get("#custom-header").then($header => { + cy.get("#custom-header").then($header => { const dialog = document.getElementById("test-dialog") as Dialog; - const header = $header.get(0) as HTMLElement; + const header = $header.get(0); // Create a mock event let preventDefaultCalled = false; @@ -1850,3 +1850,315 @@ describe("Native drag-and-drop in draggable dialogs", () => { }); }); }); + +describe("Fullscreen Button", () => { + it("should show fullscreen button when showFullscreenButton is set", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .should("exist") + .and("be.visible"); + }); + + it("should not show fullscreen button when showFullscreenButton is not set", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .should("not.exist"); + }); + + it("should toggle stretch property when fullscreen button is clicked", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .should("not.have.attr", "stretch"); + + cy.get("#dialog").then($dialog => { + $dialog.get(0)._toggleFullscreen(); + }); + + cy.get("#dialog") + .should("have.attr", "stretch"); + + cy.get("#dialog").then($dialog => { + $dialog.get(0)._toggleFullscreen(); + }); + + cy.get("#dialog") + .should("not.have.attr", "stretch"); + }); + + it("should show full-screen icon when not stretched and exit-full-screen when stretched", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .should("have.attr", "icon", "full-screen"); + + cy.get("#dialog").then($dialog => { + $dialog.get(0)._toggleFullscreen(); + }); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .should("have.attr", "icon", "exit-full-screen"); + }); + + it("should have correct tooltip based on stretch state", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .should("have.attr", "tooltip", "Maximize (Shift+Ctrl+F)"); + + cy.get("#dialog").then($dialog => { + $dialog.get(0)._toggleFullscreen(); + }); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .should("have.attr", "tooltip", "Restore (Shift+Ctrl+F)"); + }); + + it("should have aria-keyshortcuts attribute", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .shadow() + .find("button") + .should("have.attr", "aria-keyshortcuts", "Shift+Ctrl+F"); + }); + + it("should toggle fullscreen with Shift+Ctrl+F keyboard shortcut", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .should("not.have.attr", "stretch"); + + cy.get("#input").realClick(); + cy.realPress(["Shift", "Control", "f"]); + + cy.get("#dialog") + .should("have.attr", "stretch"); + + cy.realPress(["Shift", "Control", "f"]); + + cy.get("#dialog") + .should("not.have.attr", "stretch"); + }); + + it("should toggle fullscreen on header double-click", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .should("not.have.attr", "stretch"); + + cy.get("#dialog").then($dialog => { + const dialog = $dialog.get(0); + const headerRoot = dialog.shadowRoot!.querySelector(".ui5-popup-header-root")!; + const event = new MouseEvent("dblclick", { bubbles: true, composed: true }); + Object.defineProperty(event, "target", { value: headerRoot }); + headerRoot.dispatchEvent(event); + }); + + cy.get("#dialog") + .should("have.attr", "stretch"); + }); + + it("should not toggle fullscreen on double-click when showFullscreenButton is false", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-popup-header-root") + .realClick({ clickCount: 2 }); + + cy.get("#dialog") + .should("not.have.attr", "stretch"); + }); + + it("should reset drag/resize state when toggling fullscreen", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog").then($dialog => { + const dialog = $dialog.get(0); + dialog._draggedOrResized = true; + Object.assign(dialog.style, { top: "100px", left: "100px", width: "400px", height: "300px" }); + }); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .realClick(); + + cy.get("#dialog").then($dialog => { + const dialog = $dialog.get(0); + expect(dialog._draggedOrResized).to.be.false; + expect(dialog.style.width).to.equal(""); + expect(dialog.style.height).to.equal(""); + }); + }); + + it("should display header when only showFullscreenButton is set", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-popup-header-root") + .should("exist"); + }); + + it("should reflect stretch state if stretch is initially true", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .should("have.attr", "icon", "exit-full-screen") + .and("have.attr", "tooltip", "Restore (Shift+Ctrl+F)"); + + cy.get("#dialog").invoke("prop", "open", false); + }); + + it("should not focus fullscreen button as initial focus when content has focusable elements", () => { + cy.mount( + + + + ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#content-btn").should("be.focused"); + }); + + it("should focus fullscreen button when no other focusable elements exist", () => { + cy.mount( + +

Non-focusable content

+
+ ); + + cy.get("#dialog").ui5DialogOpened(); + + cy.get("#dialog") + .shadow() + .find(".ui5-dialog-fullscreen-btn") + .should("be.focused"); + }); + + it("Ctrl+Shift+F should only toggle the top dialog when two dialogs with fullscreen button are open", () => { + cy.mount( + <> + + + + + + + + ); + + cy.get("#dialog1").invoke("prop", "open", true); + cy.get("#dialog1").ui5DialogOpened(); + + cy.get("#dialog2").invoke("prop", "open", true); + cy.get("#dialog2").ui5DialogOpened(); + + cy.get("#dialog1").should("not.have.attr", "stretch"); + cy.get("#dialog2").should("not.have.attr", "stretch"); + + cy.get("#input2").realClick(); + cy.realPress(["Shift", "Control", "f"]); + + cy.get("#dialog2").should("have.attr", "stretch"); + cy.get("#dialog1").should("not.have.attr", "stretch"); + + cy.realPress(["Shift", "Control", "f"]); + + cy.get("#dialog2").should("not.have.attr", "stretch"); + cy.get("#dialog1").should("not.have.attr", "stretch"); + }); +}); diff --git a/packages/main/src/Dialog.ts b/packages/main/src/Dialog.ts index f7f3a16a7a60..be6f4111cebe 100644 --- a/packages/main/src/Dialog.ts +++ b/packages/main/src/Dialog.ts @@ -11,11 +11,14 @@ import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import toLowercaseEnumValue from "@ui5/webcomponents-base/dist/util/toLowercaseEnumValue.js"; +import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js"; import Popup from "./Popup.js"; import "@ui5/webcomponents-icons/dist/error.js"; import "@ui5/webcomponents-icons/dist/alert.js"; import "@ui5/webcomponents-icons/dist/sys-enter-2.js"; import "@ui5/webcomponents-icons/dist/information.js"; +import "@ui5/webcomponents-icons/dist/full-screen.js"; +import "@ui5/webcomponents-icons/dist/exit-full-screen.js"; import { DIALOG_ARIA_DESCRIBEDBY_RESIZABLE, @@ -32,6 +35,8 @@ import { DIALOG_HEADER_ARIA_LABEL, DIALOG_CONTENT_ARIA_LABEL, DIALOG_FOOTER_ARIA_LABEL, + DIALOG_FULLSCREEN_MAXIMIZE, + DIALOG_FULLSCREEN_RESTORE, } from "./generated/i18n/i18n-defaults.js"; // Template @@ -46,6 +51,10 @@ import PopupAccessibleRole from "./types/PopupAccessibleRole.js"; */ const STEP_SIZE = 16; +const FULLSCREEN_BUTTON_ACCESSIBILITY_ATTRIBUTES = { + ariaKeyShortcuts: "Shift+Ctrl+F", +}; + type ValueStateWithIcon = ValueState.Negative | ValueState.Critical | ValueState.Positive | ValueState.Information; /** * Defines the icons corresponding to the dialog's state. @@ -101,6 +110,12 @@ const ICON_PER_STATE: Record = { * - [Shift] + [Up] or [Down] - Decrease/Increase the height of the dialog. * - [Shift] + [Left] or [Right] - Decrease/Increase the width of the dialog. * + * #### Fullscreen + * When the `ui5-dialog` has the `showFullscreenButton` property set to `true`, the user can toggle fullscreen mode + * with the following keyboard shortcut: + * + * - [Shift] + [Ctrl] + [F] - Toggle fullscreen mode. + * * ### ES6 Module Import * * `import "@ui5/webcomponents/dist/Dialog";` @@ -176,6 +191,21 @@ class Dialog extends Popup { @property({ type: Boolean }) resizable = false; + /** + * Defines whether a fullscreen toggle button is shown in the dialog header. + * When pressed, it toggles the `stretch` property. + * The fullscreen button is not available on phone devices. + * + * **Note:** The fullscreen button is not available on phone devices, + * nor when a custom header slot is provided — the application is expected + * to render its own toggle inside the custom header in those cases. + * @default false + * @since 2.25.0 + * @public + */ + @property({ type: Boolean }) + showFullscreenButton = false; + /** * Defines the state of the `Dialog`. * @@ -188,12 +218,19 @@ class Dialog extends Popup { @property() state: `${ValueState}` = "None"; + /** + * @private + */ + @property({ type: Boolean }) + _showFullscreenButton = false; + _screenResizeHandler: () => void; _dragMouseMoveHandler: (e: MouseEvent) => void; _dragMouseUpHandler: (e: MouseEvent) => void; _resizeMouseMoveHandler: (e: MouseEvent) => void; _resizeMouseUpHandler: (e: MouseEvent) => void; _dragStartHandler: (e: DragEvent) => void; + _fullscreenKeydownHandler: (e: KeyboardEvent) => void; _y?: number; _x?: number; _isRTL?: boolean; @@ -208,6 +245,7 @@ class Dialog extends Popup { _cachedMinHeight?: number; _draggedOrResized = false; _dragHandlerRegistered = false; + _fullscreenKeydownHandlerRegistered = false; /** * Defines the header HTML Element. @@ -242,6 +280,7 @@ class Dialog extends Popup { this._resizeMouseUpHandler = this._onResizeMouseUp.bind(this); this._dragStartHandler = this._handleDragStart.bind(this); + this._fullscreenKeydownHandler = this._onFullscreenKeydown.bind(this); } static _isHeader(element: HTMLElement) { @@ -320,7 +359,7 @@ class Dialog extends Popup { * Determines if the header should be shown. */ get _displayHeader() { - return this.header.length || this.headerText || this.draggable || this.resizable; + return this.header.length || this.headerText || this.draggable || this.resizable || this._showFullscreenButton; } get _movable() { @@ -358,7 +397,21 @@ class Dialog extends Popup { } get _showResizeHandle() { - return this.resizable && this.onDesktop; + return this.resizable && this.onDesktop && !this.stretch; + } + + get _fullscreenButtonIcon() { + return this.stretch ? "exit-full-screen" : "full-screen"; + } + + get _fullscreenButtonTooltip() { + return this.stretch + ? Dialog.i18nBundle.getText(DIALOG_FULLSCREEN_RESTORE) + : Dialog.i18nBundle.getText(DIALOG_FULLSCREEN_MAXIMIZE); + } + + get _fullscreenButtonAccessibilityAttributes() { + return FULLSCREEN_BUTTON_ACCESSIBILITY_ATTRIBUTES; } get _resizeHandleTooltip() { @@ -425,6 +478,8 @@ class Dialog extends Popup { onBeforeRendering() { super.onBeforeRendering(); + this._showFullscreenButton = this.showFullscreenButton && !this.onPhone && !this.header.length; + this._isRTL = this.effectiveDir === "rtl"; } @@ -446,11 +501,13 @@ class Dialog extends Popup { _attachBrowserEvents() { this._attachScreenResizeHandler(); this._registerDragHandler(); + this._registerFullscreenKeydownHandler(); } _detachBrowserEvents() { this._detachScreenResizeHandler(); this._deregisterDragHandler(); + this._deregisterFullscreenKeydownHandler(); } _attachScreenResizeHandler() { @@ -481,6 +538,20 @@ class Dialog extends Popup { } } + _registerFullscreenKeydownHandler() { + if (this.showFullscreenButton && !this._fullscreenKeydownHandlerRegistered) { + document.addEventListener("keydown", this._fullscreenKeydownHandler); + this._fullscreenKeydownHandlerRegistered = true; + } + } + + _deregisterFullscreenKeydownHandler() { + if (this._fullscreenKeydownHandlerRegistered) { + document.removeEventListener("keydown", this._fullscreenKeydownHandler); + this._fullscreenKeydownHandlerRegistered = false; + } + } + _center() { const height = window.innerHeight - this.offsetHeight, width = window.innerWidth - this.offsetWidth; @@ -503,6 +574,49 @@ class Dialog extends Popup { /** * Event handlers */ + _toggleFullscreen() { + if (this.onPhone) { + return; + } + + const wasStretched = this.stretch; + this.stretch = !this.stretch; + + this._revertSize(); + this._draggedOrResized = false; + + if (wasStretched) { + requestAnimationFrame(() => { + if (this.open) { + this._center(); + } + }); + } + } + + _onHeaderDblClick(e: MouseEvent) { + const target = e.target as HTMLElement; + const headerRoot = this._root.querySelector(".ui5-popup-header-root"); + if (target !== headerRoot && !target.classList.contains("ui5-popup-header-text")) { + return; + } + + this._toggleFullscreen(); + } + + _onFullscreenKeydown(e: KeyboardEvent) { + if (this.isTopModalPopup && this._showFullscreenButton && this._isFullscreenShortcut(e)) { + e.preventDefault(); + e.stopImmediatePropagation(); + + this._toggleFullscreen(); + } + } + + _isFullscreenShortcut(e: KeyboardEvent) { + return (e.key === "f" || e.key === "F") && e.ctrlKey && e.shiftKey && !e.altKey; + } + _onDragMouseDown(e: MouseEvent) { // allow dragging only on the header if (!this._movable || !this.draggable || !Dialog._isHeader(e.target as HTMLElement)) { @@ -779,6 +893,15 @@ class Dialog extends Popup { window.removeEventListener("mouseup", this._resizeMouseUpHandler); } + async _getFirstFocusableElement() { + if (this._showFullscreenButton) { + const firstFocusable = await getFirstFocusableElement(this.contentDOM) || (this.footerDOM ? await getFirstFocusableElement(this.footerDOM) : null); + return firstFocusable || getFirstFocusableElement(this); + } + + return getFirstFocusableElement(this); + } + /** * Overrides Popup's forwardToLast to prioritize the drag/resize handler * when Shift+Tab is pressed from the first focusable element. diff --git a/packages/main/src/DialogTemplate.tsx b/packages/main/src/DialogTemplate.tsx index 6880028ffa73..6020332ac300 100644 --- a/packages/main/src/DialogTemplate.tsx +++ b/packages/main/src/DialogTemplate.tsx @@ -3,6 +3,7 @@ import type Dialog from "./Dialog.js"; import PopupTemplate from "./PopupTemplate.js"; import Title from "./Title.js"; import Icon from "./Icon.js"; +import Button from "./Button.js"; export default function DialogTemplate(this: Dialog) { return PopupTemplate.call(this, { @@ -20,6 +21,7 @@ function beforeContent(this: Dialog) { role="region" aria-label={this._headerAriaLabel} onMouseDown={this._onDragMouseDown} + onDblClick={this._showFullscreenButton ? this._onHeaderDblClick : undefined} part="header" // state={this.state} > @@ -31,6 +33,17 @@ function beforeContent(this: Dialog) { : {this.headerText} } + {this._showFullscreenButton && + + } } ); diff --git a/packages/main/src/Popup.ts b/packages/main/src/Popup.ts index 506d163c619b..8c3e891f21ff 100644 --- a/packages/main/src/Popup.ts +++ b/packages/main/src/Popup.ts @@ -542,7 +542,7 @@ abstract class Popup extends UI5Element { || document.getElementById(this.initialFocus); } - element = element || await getFirstFocusableElement(this) || this._root; // in case of no focusable content focus the root + element = element || await this._getFirstFocusableElement() || this._root; // in case of no focusable content focus the root if (element) { if (element === this._root) { @@ -552,6 +552,10 @@ abstract class Popup extends UI5Element { } } + async _getFirstFocusableElement() { + return getFirstFocusableElement(this); + } + isFocusWithin() { return isFocusedElementWithinNode(this._root); } @@ -720,6 +724,10 @@ abstract class Popup extends UI5Element { return this.shadowRoot!.querySelector(".ui5-popup-content")!; } + get footerDOM(): HTMLElement | null { + return this.shadowRoot!.querySelector(".ui5-popup-footer-root"); + } + get styles() { return { root: {}, diff --git a/packages/main/src/i18n/messagebundle.properties b/packages/main/src/i18n/messagebundle.properties index 6176393b46c1..98ce112d6441 100644 --- a/packages/main/src/i18n/messagebundle.properties +++ b/packages/main/src/i18n/messagebundle.properties @@ -873,6 +873,13 @@ DIALOG_CONTENT_ARIA_LABEL=Content #XACT: ARIA label for the Dialog footer region DIALOG_FOOTER_ARIA_LABEL=Footer + +#XACT: Tooltip for dialog fullscreen button (maximize) +DIALOG_FULLSCREEN_MAXIMIZE=Maximize (Shift+Ctrl+F) + +#XACT: Tooltip for dialog fullscreen button (restore) +DIALOG_FULLSCREEN_RESTORE=Restore (Shift+Ctrl+F) + #XFLD: A colon to separate the "label" from an input. In some languages there might be a different symbol used for such a colon LABEL_COLON=: diff --git a/packages/main/src/themes/Dialog.css b/packages/main/src/themes/Dialog.css index bd4001653e57..31b25dadfa4e 100644 --- a/packages/main/src/themes/Dialog.css +++ b/packages/main/src/themes/Dialog.css @@ -29,7 +29,7 @@ cursor: move; } -:host([draggable]) .ui5-popup-header-root * { +:host([draggable]) .ui5-popup-header-root *:not(.ui5-dialog-fullscreen-btn) { cursor: auto; } @@ -125,6 +125,18 @@ height: 100%; } +:host(:not([header-text])[_show-fullscreen-button]) .ui5-popup-header-root { + justify-content: flex-end; +} + +:host([header-text][_show-fullscreen-button]) .ui5-popup-header-root { + justify-content: space-between; +} + +.ui5-popup-header-root { + gap: 0.5rem; +} + .ui5-popup-content { min-height: var(--_ui5_dialog_content_min_height); flex: 1 1 auto; @@ -144,6 +156,15 @@ color: var(--sapButton_Lite_TextColor); } +.ui5-popup-header-text { + min-width: 0; + justify-content: flex-start; +} + +:host([on-phone]) .ui5-dialog-fullscreen-btn { + display: none; +} + :host::backdrop { background-color: var(--_ui5_popup_block_layer_background); opacity: var(--_ui5_popup_block_layer_opacity); diff --git a/packages/main/test/pages/Dialog.html b/packages/main/test/pages/Dialog.html index 160b30e7151e..abd635a4416a 100644 --- a/packages/main/test/pages/Dialog.html +++ b/packages/main/test/pages/Dialog.html @@ -35,6 +35,7 @@ +

Open Dialog with Input @@ -82,6 +83,12 @@ Open draggable & resizable dialog

+ Open fullscreen dialog +
+
+ Open fullscreen dialog (long title) +
+
Open RTL draggable & resizable dialog

@@ -439,6 +446,37 @@ + +

This dialog has a fullscreen toggle button in the header.

+

Press the fullscreen button or use Shift+Ctrl+F to toggle fullscreen mode.

+

You can also double-click the header to toggle.

+ +
+ + Open new fullscreen dialog + + +

This dialog has a fullscreen toggle button in the header.

+

Press the fullscreen button or use Shift+Ctrl+F to toggle fullscreen mode.

+

You can also double-click the header to toggle.

+ +
+ Close +
+
+ +
+ Close +
+
+ + +

Compare the title position with and without the fullscreen button.

+
+ Close +
+
+

Move this dialog around the screen by dragging it by its header.

@@ -898,6 +936,10 @@ scrollHelper.style.display = cbScrollable.checked ? "block" : "none"; }); + cbRtl.addEventListener("ui5-change", function () { + document.documentElement.dir = cbRtl.checked ? "rtl" : "ltr"; + }); + let preventClosing = true; btnOpenDialog.addEventListener("click", function () { @@ -1000,6 +1042,12 @@ window["resizable-custom-header-close"].addEventListener("click", function () { window["resizable-dialog-custom-header"].open = false; }); window["draggable-and-resizable-open"].addEventListener("click", function () { window["draggable-and-resizable-dialog"].open = true; }); window["draggable-and-resizable-close"].addEventListener("click", function () { window["draggable-and-resizable-dialog"].open = false; }); + window["fullscreen-open"].addEventListener("click", function () { window["fullscreen-dialog"].open = true; }); + window["fullscreen-close"].addEventListener("click", function () { window["fullscreen-dialog"].open = false; }); + window["fullscreen-nested-open"].addEventListener("click", function () { window["fullscreen-dialog-nested"].open = true; }); + window["fullscreen-nested-close"].addEventListener("click", function () { window["fullscreen-dialog-nested"].open = false; }); + window["fullscreen-long-title-open"].addEventListener("click", function () { window["fullscreen-long-title"].open = true; }); + window["fullscreen-long-title-close"].addEventListener("click", function () { window["fullscreen-long-title"].open = false; }); window["rtl-draggable-and-resizable-open"].addEventListener("click", function () { window["rtl-draggable-and-resizable-dialog"].open = true; }); window["rtl-draggable-and-resizable-close"].addEventListener("click", function () { window["rtl-draggable-and-resizable-dialog"].open = false; }); window["rtl-maxwidth-resizable-open"].addEventListener("click", function () { window["rtl-maxwidth-resizable-dialog"].open = true; }); diff --git a/packages/website/docs/_components_pages/main/Dialog.mdx b/packages/website/docs/_components_pages/main/Dialog.mdx index 403387ab4706..539c059b0c53 100644 --- a/packages/website/docs/_components_pages/main/Dialog.mdx +++ b/packages/website/docs/_components_pages/main/Dialog.mdx @@ -4,6 +4,7 @@ slug: ../Dialog import Basic from "../../_samples/main/Dialog/Basic/Basic.md"; import DraggableAndResizable from "../../_samples/main/Dialog/DraggableAndResizable/DraggableAndResizable.md"; +import Fullscreen from "../../_samples/main/Dialog/Fullscreen/Fullscreen.md"; import BarInDialog from "../../_samples/main/Dialog/BarInDialog/BarInDialog.md"; import WithState from "../../_samples/main/Dialog/WithState/WithState.md"; @@ -19,6 +20,10 @@ import WithState from "../../_samples/main/Dialog/WithState/WithState.md"; ### Draggable and Resizable +### Fullscreen +Users can toggle between standard and full screen size. The full screen button is positioned top-right in the dialog's title bar. The fullscreen toggle can also be triggered by pressing Shift+Ctrl+F or by double-clicking the dialog header. + + ### Usage of Bar as header/footer The Bar component can be used as header and/or footer of the Dialog diff --git a/packages/website/docs/_samples/main/Dialog/Fullscreen/Fullscreen.md b/packages/website/docs/_samples/main/Dialog/Fullscreen/Fullscreen.md new file mode 100644 index 000000000000..0c062a836e84 --- /dev/null +++ b/packages/website/docs/_samples/main/Dialog/Fullscreen/Fullscreen.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + diff --git a/packages/website/docs/_samples/main/Dialog/Fullscreen/main.js b/packages/website/docs/_samples/main/Dialog/Fullscreen/main.js new file mode 100644 index 000000000000..4f897dd252e1 --- /dev/null +++ b/packages/website/docs/_samples/main/Dialog/Fullscreen/main.js @@ -0,0 +1,21 @@ +import "@ui5/webcomponents/dist/Dialog.js"; +import "@ui5/webcomponents/dist/Button.js"; +import "@ui5/webcomponents/dist/Toolbar.js"; +import "@ui5/webcomponents/dist/ToolbarButton.js"; + +const dialogOpener = document.getElementById("dialogOpener"); +const dialog = document.getElementById("dialog"); +const dialogClosers = [...dialog.querySelectorAll(".dialogCloser")]; + +dialogOpener.accessibilityAttributes = { + hasPopup: "dialog", + controls: dialog.id, +}; +dialogOpener.addEventListener("click", () => { + dialog.open = true; +}); +dialogClosers.forEach(btn => { + btn.addEventListener("click", () => { + dialog.open = false; + }); +}) diff --git a/packages/website/docs/_samples/main/Dialog/Fullscreen/sample.html b/packages/website/docs/_samples/main/Dialog/Fullscreen/sample.html new file mode 100644 index 000000000000..b9ab5882749a --- /dev/null +++ b/packages/website/docs/_samples/main/Dialog/Fullscreen/sample.html @@ -0,0 +1,29 @@ + + + + + + + + Sample + + + + + + Open Dialog with Fullscreen Button + + +
This dialog has a fullscreen toggle button in the header.
+
Click the fullscreen button or press Shift+Ctrl+F to toggle fullscreen mode.
+
You can also double-click the header to toggle.
+ + + +
+ + + + + + diff --git a/packages/website/docs/_samples/main/Dialog/Fullscreen/sample.tsx b/packages/website/docs/_samples/main/Dialog/Fullscreen/sample.tsx new file mode 100644 index 000000000000..9bae0850e0f4 --- /dev/null +++ b/packages/website/docs/_samples/main/Dialog/Fullscreen/sample.tsx @@ -0,0 +1,50 @@ +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import { useState } from "react"; +import ButtonClass from "@ui5/webcomponents/dist/Button.js"; +import DialogClass from "@ui5/webcomponents/dist/Dialog.js"; +import ToolbarClass from "@ui5/webcomponents/dist/Toolbar.js"; +import ToolbarButtonClass from "@ui5/webcomponents/dist/ToolbarButton.js"; + +const Button = createReactComponent(ButtonClass); +const Dialog = createReactComponent(DialogClass); +const Toolbar = createReactComponent(ToolbarClass); +const ToolbarButton = createReactComponent(ToolbarButtonClass); + +function App() { + const [dialogOpen, setDialogOpen] = useState(false); + + return ( + <> + + + setDialogOpen(false)} + > +
This dialog has a fullscreen toggle button in the header.
+
+ Click the fullscreen button or press Shift+Ctrl+F to toggle fullscreen + mode. +
+
You can also double-click the header to toggle.
+ + setDialogOpen(false)} + /> + +
+ + ); +} + +export default App; From 7ed08efc1bb36f37ac24f8a7887d03557ef16341 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov <31909318+nnaydenow@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:52:34 +0300 Subject: [PATCH 6/7] test: add visual regression tests for main component (#13797) --- .claude/skills/visual-test/SKILL.md | 190 ++++++++- .../cypress/specs/visuals/AvatarBadge.cy.tsx | 94 +++++ .../cypress/specs/visuals/AvatarGroup.cy.tsx | 84 ++++ .../main/cypress/specs/visuals/Bar.cy.tsx | 98 +++++ .../cypress/specs/visuals/Breadcrumbs.cy.tsx | 101 +++++ .../cypress/specs/visuals/ButtonBadge.cy.tsx | 99 +++++ .../cypress/specs/visuals/Calendar.cy.tsx | 109 +++++ .../specs/visuals/CalendarLegend.cy.tsx | 90 +++++ .../main/cypress/specs/visuals/Card.cy.tsx | 116 ++++++ .../cypress/specs/visuals/Carousel.cy.tsx | 120 ++++++ .../cypress/specs/visuals/ColorPalette.cy.tsx | 83 ++++ .../specs/visuals/ColorPalettePopover.cy.tsx | 78 ++++ .../cypress/specs/visuals/ColorPicker.cy.tsx | 55 +++ .../cypress/specs/visuals/ComboBox.cy.tsx | 340 +++++++++++++++- .../cypress/specs/visuals/DatePicker.cy.tsx | 81 ++++ .../specs/visuals/DateRangePicker.cy.tsx | 146 +++++++ .../specs/visuals/DateTimePicker.cy.tsx | 81 ++++ .../main/cypress/specs/visuals/Dialog.cy.tsx | 114 ++++++ .../specs/visuals/DynamicDateRange.cy.tsx | 82 ++++ .../specs/visuals/ExpandableText.cy.tsx | 68 ++++ .../cypress/specs/visuals/FileUploader.cy.tsx | 90 +++++ .../main/cypress/specs/visuals/Form.cy.tsx | 235 +++++++++++ .../main/cypress/specs/visuals/Icon.cy.tsx | 92 +++++ .../main/cypress/specs/visuals/Input.cy.tsx | 40 ++ .../main/cypress/specs/visuals/Label.cy.tsx | 50 +++ .../main/cypress/specs/visuals/Link.cy.tsx | 66 +++ .../main/cypress/specs/visuals/List.cy.tsx | 132 ++++++ .../specs/visuals/ListItemGroup.cy.tsx | 85 ++++ .../specs/visuals/ListItemStandard.cy.tsx | 142 +++++++ .../main/cypress/specs/visuals/Menu.cy.tsx | 220 ++++++++++ .../specs/visuals/MultiComboBox.cy.tsx | 264 +++++++++++- .../cypress/specs/visuals/MultiInput.cy.tsx | 183 +++++++++ .../main/cypress/specs/visuals/Panel.cy.tsx | 78 ++++ .../main/cypress/specs/visuals/Popover.cy.tsx | 105 +++++ .../specs/visuals/ProgressIndicator.cy.tsx | 72 ++++ .../cypress/specs/visuals/RangeSlider.cy.tsx | 52 +++ .../specs/visuals/RatingIndicator.cy.tsx | 69 ++++ .../specs/visuals/ResponsivePopover.cy.tsx | 80 ++++ .../specs/visuals/SegmentedButton.cy.tsx | 97 +++++ .../main/cypress/specs/visuals/Select.cy.tsx | 197 +++++++++ .../main/cypress/specs/visuals/Slider.cy.tsx | 52 +++ .../specs/visuals/SpecialCalendarDate.cy.tsx | 36 ++ .../cypress/specs/visuals/SplitButton.cy.tsx | 66 +++ .../cypress/specs/visuals/StepInput.cy.tsx | 67 ++++ .../specs/visuals/SuggestionItem.cy.tsx | 47 +++ .../specs/visuals/SuggestionItemCustom.cy.tsx | 33 ++ .../specs/visuals/SuggestionItemGroup.cy.tsx | 46 +++ .../cypress/specs/visuals/TabContainer.cy.tsx | 375 ++++++++++++++++++ .../main/cypress/specs/visuals/Text.cy.tsx | 94 +++++ .../cypress/specs/visuals/TextArea.cy.tsx | 134 +++++++ .../cypress/specs/visuals/TimePicker.cy.tsx | 127 ++++++ .../main/cypress/specs/visuals/Title.cy.tsx | 68 ++++ .../main/cypress/specs/visuals/Toast.cy.tsx | 77 ++++ .../cypress/specs/visuals/ToggleButton.cy.tsx | 124 ++++++ .../main/cypress/specs/visuals/Token.cy.tsx | 62 +++ .../cypress/specs/visuals/Tokenizer.cy.tsx | 100 +++++ .../main/cypress/specs/visuals/Toolbar.cy.tsx | 67 ++++ .../specs/visuals/ToolbarButton.cy.tsx | 80 ++++ .../cypress/specs/visuals/ToolbarItem.cy.tsx | 57 +++ .../specs/visuals/ToolbarSelect.cy.tsx | 88 ++++ .../specs/visuals/ToolbarSeparator.cy.tsx | 44 ++ .../specs/visuals/ToolbarSpacer.cy.tsx | 42 ++ .../main/cypress/specs/visuals/Tree.cy.tsx | 134 +++++++ .../cypress/specs/visuals/TreeItem.cy.tsx | 129 ++++++ .../specs/visuals/TreeItemCustom.cy.tsx | 108 +++++ 65 files changed, 6803 insertions(+), 32 deletions(-) create mode 100644 packages/main/cypress/specs/visuals/AvatarBadge.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/AvatarGroup.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Bar.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Breadcrumbs.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ButtonBadge.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Calendar.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/CalendarLegend.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Card.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Carousel.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ColorPalette.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ColorPalettePopover.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ColorPicker.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/DateRangePicker.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Dialog.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/DynamicDateRange.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ExpandableText.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/FileUploader.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Form.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Icon.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Label.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Link.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/List.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ListItemGroup.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ListItemStandard.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Menu.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/MultiInput.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Panel.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Popover.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ProgressIndicator.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/RangeSlider.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/RatingIndicator.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ResponsivePopover.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/SegmentedButton.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Slider.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/SpecialCalendarDate.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/SplitButton.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/StepInput.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/SuggestionItem.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/SuggestionItemCustom.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/SuggestionItemGroup.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/TabContainer.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Text.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/TextArea.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/TimePicker.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Title.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Toast.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ToggleButton.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Token.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Tokenizer.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Toolbar.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ToolbarButton.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ToolbarItem.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ToolbarSelect.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ToolbarSeparator.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/ToolbarSpacer.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/Tree.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/TreeItem.cy.tsx create mode 100644 packages/main/cypress/specs/visuals/TreeItemCustom.cy.tsx diff --git a/.claude/skills/visual-test/SKILL.md b/.claude/skills/visual-test/SKILL.md index 1744966123e2..f2894ba1503f 100644 --- a/.claude/skills/visual-test/SKILL.md +++ b/.claude/skills/visual-test/SKILL.md @@ -90,6 +90,18 @@ For `IllustratedMessage`, there are no individual illustration source files — import "../../../src/illustrations/AllIllustrations.js"; ``` +When a component uses an `icon` prop, import each icon as a **named import** (not a side-effect import) and pass the imported value to the prop: + +```tsx +// WRONG — side-effect import, icon name passed as string +import "@ui5/webcomponents-icons/dist/favorite.js"; + + +// CORRECT — named import, value passed to prop +import favorite from "@ui5/webcomponents-icons/dist/favorite.js"; + +``` + Import only what you actually use in the test. ## Before writing: read the existing functional test @@ -148,8 +160,19 @@ Look at what the component can show and add cases accordingly: narrowed list. - **Selected items / tokens** — pre-select items, screenshot the tokenized state. -- **Value state** — when a component accepts `valueState`, write one `it` per - value state value. The full set is `Negative`, `Critical`, `Positive`, +- **Value state** — when a component accepts `valueState`, write **three** `it` + blocks per value state value: + 1. **Basic** — component mounted with the value state prop, no interaction (screenshot at rest). + 2. **Popover/dropdown open** — for components that have a dropdown or calendar + popover (ComboBox, MultiComboBox, Select, DatePicker, DateRangePicker, + DateTimePicker, MultiInput), open it and screenshot. The value state message + strip appears inside the popover here. + 3. **Focused, dropdown closed** — for the same components, open then close with + Escape (or simply click the input area) so the component is focused but the + dropdown is closed. A standalone value state message popover appears in this + state, which has different rendering to the in-dropdown version. + For Input and MultiInput (no dropdown), just click the input to focus. + The full set of value state values is `Negative`, `Critical`, `Positive`, `Information`. `None` is the default and is already covered by the basic state test — do not add a separate case for it. Always include a `valueStateMessage` slot so the message strip renders. Applies to Input, @@ -203,6 +226,22 @@ to themselves and is more resilient to tag name changes. Always prefer the existing custom commands over raw DOM queries — they already handle waiting and shadow DOM traversal correctly. +### Nested shadow DOM — date/time pickers + +`DatePicker`, `DateRangePicker`, and `DateTimePicker` render their text field +as an internal `ui5-datetime-input` component, which has **its own shadow +DOM**. The native `` is therefore two shadow levels deep. Use: + +```tsx +cy.get("[ui5-date-picker]") + .shadow().find("ui5-datetime-input") + .shadow().find("input") + .realClick(); +``` + +`shadow().find("input")` on the outer picker element will fail with +"Expected to find element: input, but never found it." + ### Available custom commands (use these for interactions) | Component | Command | What it does | @@ -315,6 +354,31 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); + it("value state — Negative — dropdown open", () => { + cy.mount( + + Error message + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Negative — focused", () => { + cy.mount( + + Error message + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Critical", () => { cy.mount( @@ -326,6 +390,31 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); + it("value state — Critical — dropdown open", () => { + cy.mount( + + Warning message + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount( + + Warning message + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Positive", () => { cy.mount( @@ -337,6 +426,31 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); + it("value state — Positive — dropdown open", () => { + cy.mount( + + Success message + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount( + + Success message + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Information", () => { cy.mount( @@ -348,6 +462,31 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); + it("value state — Information — dropdown open", () => { + cy.mount( + + Info message + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount( + + Info message + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("no items — empty dropdown open", () => { cy.mount(); cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); @@ -374,17 +513,50 @@ runs will cause false diffs and make the test useless as a visual baseline. **Avoid:** - `BusyIndicator` — the spinner animation frame varies per run. -- `DatePicker`, `TimePicker`, `DateRangePicker`, `DateTimePicker` without a - fixed value — they display today's date, which changes daily. Always pass - a hardcoded `value` prop: ``. -- `DynamicDateRange` without a fixed option selected. - Any component that fetches or derives content at runtime (clocks, relative timestamps, random IDs shown in the UI). -**If you must include a date/time component**, pin the value: +### Freezing time for date/time components + +Any component that calls `new Date()` internally — to format a value, +highlight today in a calendar, or derive a relative label — will produce +different screenshots on different days. This includes: + +- `Calendar`, `SpecialCalendarDate` — today's cell is highlighted +- `DatePicker`, `DateRangePicker`, `DateTimePicker` — today is highlighted in the open calendar +- `DynamicDateRange` — operators like `TODAY`, `LASTDAYS`, `NEXTWEEKS` format to the current date + +**Always add a `beforeEach` that freezes `Date` using `cy.clock`:** + +```tsx +beforeEach(() => { + cy.clock(new Date("Jan 15, 2024").getTime(), ["Date"]); +}); +``` + +The second argument `["Date"]` is critical — it stubs **only** the `Date` +constructor and leaves `setTimeout`/`setInterval` untouched. Without it, +`cy.clock()` also freezes timers, which breaks UI5's async rendering and +causes all tests to time out. + +Use `Jan 15, 2024` as the standard fixed date across all tests in this +repo for consistency. + +For `Calendar` and `SpecialCalendarDate`, pass a fixed `timestamp` prop +instead of (or in addition to) `cy.clock` — the `timestamp` prop pins +which month is displayed: + +```tsx + // Jan 15, 2024 in epoch seconds +``` + +For `DynamicDateRange`, also import all option classes so they register +themselves before the component renders: + ```tsx - - +import "../../../src/dynamic-date-range-options/Today.js"; +import "../../../src/dynamic-date-range-options/SingleDate.js"; +// ... all options used in the test ``` ## Running the test diff --git a/packages/main/cypress/specs/visuals/AvatarBadge.cy.tsx b/packages/main/cypress/specs/visuals/AvatarBadge.cy.tsx new file mode 100644 index 000000000000..c57255facff7 --- /dev/null +++ b/packages/main/cypress/specs/visuals/AvatarBadge.cy.tsx @@ -0,0 +1,94 @@ +import Avatar from "../../../src/Avatar.js"; +import AvatarBadge from "../../../src/AvatarBadge.js"; +import "@ui5/webcomponents-icons/dist/employee.js"; +import "@ui5/webcomponents-icons/dist/alert.js"; + +describe("AvatarBadge visual", () => { + it("basic state — icon badge on avatar", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("value state — None", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("value state — Positive", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("value state — Negative", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("value state — Information", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("all sizes", () => { + cy.mount( +
+ + + + + + + + + + + + + + + +
+ ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/AvatarGroup.cy.tsx b/packages/main/cypress/specs/visuals/AvatarGroup.cy.tsx new file mode 100644 index 000000000000..a5f51d901408 --- /dev/null +++ b/packages/main/cypress/specs/visuals/AvatarGroup.cy.tsx @@ -0,0 +1,84 @@ +import Avatar from "../../../src/Avatar.js"; +import AvatarGroup from "../../../src/AvatarGroup.js"; + +describe("AvatarGroup visual", () => { + it("type Group — initials", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("type Individual — initials", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("type Group — size XS", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("type Group — size L", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("overflow — many avatars", () => { + cy.mount( +
+ + + + + + + + + + +
+ ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Bar.cy.tsx b/packages/main/cypress/specs/visuals/Bar.cy.tsx new file mode 100644 index 000000000000..d5eefe057cda --- /dev/null +++ b/packages/main/cypress/specs/visuals/Bar.cy.tsx @@ -0,0 +1,98 @@ +import Bar from "../../../src/Bar.js"; +import Button from "../../../src/Button.js"; + +describe("Bar visual", () => { + it("basic state — Header design", () => { + cy.mount( + + +
Page Title
+ +
+ ); + cy.screenshot(); + }); + + it("compact mode — Header design", () => { + cy.mount( +
+ + +
Page Title
+ +
+
+ ); + cy.screenshot(); + }); + + it("design — Subheader", () => { + cy.mount( + + +
Subheader Title
+ +
+ ); + cy.screenshot(); + }); + + it("design — Footer", () => { + cy.mount( + + +
Footer Content
+ +
+ ); + cy.screenshot(); + }); + + it("design — FloatingFooter", () => { + cy.mount( + + + + + ); + cy.screenshot(); + }); + + it("start content only", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("end content only", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("middle content only", () => { + cy.mount( + +
Centered Title
+
+ ); + cy.screenshot(); + }); + + it("long content — shrinked layout", () => { + cy.mount( + + +
A Very Long Page Title That Takes Up Space
+ +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Breadcrumbs.cy.tsx b/packages/main/cypress/specs/visuals/Breadcrumbs.cy.tsx new file mode 100644 index 000000000000..c33c4ae59ad1 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Breadcrumbs.cy.tsx @@ -0,0 +1,101 @@ +import Breadcrumbs from "../../../src/Breadcrumbs.js"; +import BreadcrumbsItem from "../../../src/BreadcrumbsItem.js"; + +describe("Breadcrumbs visual", () => { + it("basic state — Standard design", () => { + cy.mount( + + Root + Products + Laptops + Current Page + + ); + cy.screenshot(); + }); + + it("design — NoCurrentPage", () => { + cy.mount( + + Root + Products + Laptops + Detail + + ); + cy.screenshot(); + }); + + it("separator — Slash (default)", () => { + cy.mount( + + Root + Section + Current + + ); + cy.screenshot(); + }); + + it("separator — GreaterThan", () => { + cy.mount( + + Root + Section + Current + + ); + cy.screenshot(); + }); + + it("separator — BackSlash", () => { + cy.mount( + + Root + Section + Current + + ); + cy.screenshot(); + }); + + it("separator — DoubleGreaterThan", () => { + cy.mount( + + Root + Section + Current + + ); + cy.screenshot(); + }); + + it("overflow — narrow container", () => { + cy.mount( +
+ + Root + Level 1 + Level 2 + Level 3 + Level 4 + Current + +
+ ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + Root + Products + Current Page + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ButtonBadge.cy.tsx b/packages/main/cypress/specs/visuals/ButtonBadge.cy.tsx new file mode 100644 index 000000000000..ea41868eaacc --- /dev/null +++ b/packages/main/cypress/specs/visuals/ButtonBadge.cy.tsx @@ -0,0 +1,99 @@ +import Button from "../../../src/Button.js"; +import ButtonBadge from "../../../src/ButtonBadge.js"; +import "@ui5/webcomponents-icons/dist/bell.js"; + +describe("ButtonBadge visual", () => { + it("design — OverlayText", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("design — InlineText", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("design — AttentionDot", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("OverlayText — large number", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("all designs side by side", () => { + cy.mount( +
+ + + +
+ ); + cy.screenshot(); + }); + + it("with icon-only button", () => { + cy.mount( +
+ + +
+ ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Calendar.cy.tsx b/packages/main/cypress/specs/visuals/Calendar.cy.tsx new file mode 100644 index 000000000000..4f0b4ab0897d --- /dev/null +++ b/packages/main/cypress/specs/visuals/Calendar.cy.tsx @@ -0,0 +1,109 @@ +import Calendar from "../../../src/Calendar.js"; +import CalendarDate from "../../../src/CalendarDate.js"; +import CalendarDateRange from "../../../src/CalendarDateRange.js"; +import CalendarLegend from "../../../src/CalendarLegend.js"; +import CalendarLegendItem from "../../../src/CalendarLegendItem.js"; + +describe("Calendar visual", () => { + it("basic state — no selection", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("selection mode — Single with selected date", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("selection mode — Multiple with selected dates", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("selection mode — Range with selected range", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("hide week numbers", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("with min and max date", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("with disabled date range", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("with calendar legend", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("month picker — opened", () => { + cy.mount( + + ); + cy.get("[ui5-calendar]").shadow().find("[data-ui5-cal-header-btn-month]").realClick(); + cy.screenshot(); + }); + + it("year picker — opened", () => { + cy.mount( + + ); + cy.get("[ui5-calendar]").shadow().find("[data-ui5-cal-header-btn-year]").realClick(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/CalendarLegend.cy.tsx b/packages/main/cypress/specs/visuals/CalendarLegend.cy.tsx new file mode 100644 index 000000000000..7c7f3fb88f0a --- /dev/null +++ b/packages/main/cypress/specs/visuals/CalendarLegend.cy.tsx @@ -0,0 +1,90 @@ +import CalendarLegend from "../../../src/CalendarLegend.js"; +import CalendarLegendItem from "../../../src/CalendarLegendItem.js"; + +describe("CalendarLegend visual", () => { + it("basic state — all built-in types", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("with custom types", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("hide today", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("hide selected day", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("hide working and non-working day", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("mixed built-in and custom types", () => { + cy.mount( + + + + + + + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Card.cy.tsx b/packages/main/cypress/specs/visuals/Card.cy.tsx new file mode 100644 index 000000000000..0369e8440ef1 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Card.cy.tsx @@ -0,0 +1,116 @@ +import Card from "../../../src/Card.js"; +import CardHeader from "../../../src/CardHeader.js"; +import Avatar from "../../../src/Avatar.js"; +import Button from "../../../src/Button.js"; +import List from "../../../src/List.js"; +import ListItemStandard from "../../../src/ListItemStandard.js"; + +describe("Card visual", () => { + it("basic state — title only", () => { + cy.mount( + + + + Item 1 + Item 2 + Item 3 + + + ); + cy.screenshot(); + }); + + it("header — title, subtitle, additional text", () => { + cy.mount( + + + + Item 1 + Item 2 + + + ); + cy.screenshot(); + }); + + it("header — with avatar slot", () => { + cy.mount( + + + + + + Alice + Bob + + + ); + cy.screenshot(); + }); + + it("header — with action slot", () => { + cy.mount( + + + + + + Q1 + Q2 + + + ); + cy.screenshot(); + }); + + it("header — interactive", () => { + cy.mount( + + + + Item 1 + Item 2 + + + ); + cy.screenshot(); + }); + + it("header — interactive focused", () => { + cy.mount( + + + + Item 1 + + + ); + cy.get("[ui5-card-header]").shadow().find(".ui5-card-header-focusable-element").focus(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + Item 1 + Item 2 + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Carousel.cy.tsx b/packages/main/cypress/specs/visuals/Carousel.cy.tsx new file mode 100644 index 000000000000..04fcbc545ed2 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Carousel.cy.tsx @@ -0,0 +1,120 @@ +import Carousel from "../../../src/Carousel.js"; +import Card from "../../../src/Card.js"; +import CardHeader from "../../../src/CardHeader.js"; +import Title from "../../../src/Title.js"; + +const CarouselPage = ({ label }: { label: string }) => ( +
+ {label} +
+); + +describe("Carousel visual", () => { + it("basic state — first page", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("page indicator — Numeric style", () => { + cy.mount( + + + + + + + + ); + cy.screenshot(); + }); + + it("hide page indicator", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("hide navigation arrows", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("arrows placement — Navigation", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("navigate to second page", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-carousel]").realHover(); + cy.get("[ui5-carousel]").shadow().find("[data-ui5-arrow-forward]").realClick(); + cy.screenshot(); + }); + + it("cyclic — on last page with dot indicator", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-carousel]").realHover(); + cy.get("[ui5-carousel]").shadow().find("[data-ui5-arrow-forward]").realClick(); + cy.get("[ui5-carousel]").shadow().find("[data-ui5-arrow-forward]").realClick(); + cy.screenshot(); + }); + + it("many pages — dot indicators (9+)", () => { + cy.mount( + + {Array.from({ length: 10 }, (_, i) => ( + + ))} + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ColorPalette.cy.tsx b/packages/main/cypress/specs/visuals/ColorPalette.cy.tsx new file mode 100644 index 000000000000..f1447a22a3f1 --- /dev/null +++ b/packages/main/cypress/specs/visuals/ColorPalette.cy.tsx @@ -0,0 +1,83 @@ +import ColorPalette from "../../../src/ColorPalette.js"; +import ColorPaletteItem from "../../../src/ColorPaletteItem.js"; + +describe("ColorPalette visual", () => { + it("basic state", () => { + cy.mount( + + + + + + + + + + + + + + ); + cy.screenshot(); + }); + + it("with selected color", () => { + cy.mount( + + + + + + + + + + ); + cy.screenshot(); + }); + + it("with default color", () => { + cy.mount( + + + + + + + + ); + cy.screenshot(); + }); + + it("with recent colors", () => { + cy.mount( + + + + + + + + ); + // select a color to populate recent colors + cy.get("[ui5-color-palette-item]").first().realClick(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ColorPalettePopover.cy.tsx b/packages/main/cypress/specs/visuals/ColorPalettePopover.cy.tsx new file mode 100644 index 000000000000..7eb83d49dba0 --- /dev/null +++ b/packages/main/cypress/specs/visuals/ColorPalettePopover.cy.tsx @@ -0,0 +1,78 @@ +import ColorPalettePopover from "../../../src/ColorPalettePopover.js"; +import ColorPaletteItem from "../../../src/ColorPaletteItem.js"; +import Button from "../../../src/Button.js"; + +describe("ColorPalettePopover visual", () => { + it("popover open — basic", () => { + cy.mount( + <> + + + + + + + + + + + + + + + ); + cy.get("[ui5-color-palette-popover]").ui5ColorPalettePopoverOpen(); + cy.screenshot(); + }); + + it("popover open — with default color", () => { + cy.mount( + <> + + + + + + + + + + ); + cy.get("[ui5-color-palette-popover]").ui5ColorPalettePopoverOpen(); + cy.screenshot(); + }); + + it("popover open — with recent colors", () => { + cy.mount( + <> + + + + + + + + + + ); + cy.get("[ui5-color-palette-popover]").ui5ColorPalettePopoverOpen(); + cy.screenshot(); + }); + + it("popover open — with show more colors", () => { + cy.mount( + <> + + + + + + + + + + ); + cy.get("[ui5-color-palette-popover]").ui5ColorPalettePopoverOpen(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ColorPicker.cy.tsx b/packages/main/cypress/specs/visuals/ColorPicker.cy.tsx new file mode 100644 index 000000000000..18e27ecebe60 --- /dev/null +++ b/packages/main/cypress/specs/visuals/ColorPicker.cy.tsx @@ -0,0 +1,55 @@ +import ColorPicker from "../../../src/ColorPicker.js"; + +describe("ColorPicker visual", () => { + it("basic state — default color", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("with red color selected", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("with blue color selected", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("with semi-transparent color", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("simplified mode — no alpha or RGB inputs", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("HSL mode toggled", () => { + cy.mount( + + ); + cy.get("[ui5-color-picker]").shadow().find("#toggle-picker-mode").realClick(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ComboBox.cy.tsx b/packages/main/cypress/specs/visuals/ComboBox.cy.tsx index ec81c3bfc8af..822693a5971a 100644 --- a/packages/main/cypress/specs/visuals/ComboBox.cy.tsx +++ b/packages/main/cypress/specs/visuals/ComboBox.cy.tsx @@ -1,5 +1,6 @@ import ComboBox from "../../../src/ComboBox.js"; import ComboBoxItem from "../../../src/ComboBoxItem.js"; +import ComboBoxItemCustom from "../../../src/ComboBoxItemCustom.js"; import ComboBoxItemGroup from "../../../src/ComboBoxItemGroup.js"; describe("ComboBox visual", () => { @@ -42,6 +43,20 @@ describe("ComboBox visual", () => { cy.screenshot(); }); + it("dropdown open — first item focused via arrow key", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.realPress("ArrowDown"); + cy.screenshot(); + }); + it("filtering — type to narrow list", () => { cy.mount( @@ -57,21 +72,14 @@ describe("ComboBox visual", () => { cy.screenshot(); }); - it("with grouped items — dropdown open", () => { + it("selected item", () => { cy.mount( - - - - - - - - - + + + + ); - cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); - cy.get("[ui5-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); cy.screenshot(); }); @@ -106,6 +114,31 @@ describe("ComboBox visual", () => { cy.screenshot(); }); + it("value state — Negative — dropdown open", () => { + cy.mount( + + Invalid value + + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Negative — focused", () => { + cy.mount( + + Invalid value + + + + ); + cy.get("[ui5-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Critical", () => { cy.mount( @@ -117,6 +150,31 @@ describe("ComboBox visual", () => { cy.screenshot(); }); + it("value state — Critical — dropdown open", () => { + cy.mount( + + Please review + + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount( + + Please review + + + + ); + cy.get("[ui5-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Positive", () => { cy.mount( @@ -128,6 +186,31 @@ describe("ComboBox visual", () => { cy.screenshot(); }); + it("value state — Positive — dropdown open", () => { + cy.mount( + + Valid selection + + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount( + + Valid selection + + + + ); + cy.get("[ui5-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Information", () => { cy.mount( @@ -139,6 +222,31 @@ describe("ComboBox visual", () => { cy.screenshot(); }); + it("value state — Information — dropdown open", () => { + cy.mount( + + Select a country + + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount( + + Select a country + + + + ); + cy.get("[ui5-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("compact mode", () => { cy.mount(
@@ -152,3 +260,211 @@ describe("ComboBox visual", () => { cy.screenshot(); }); }); + +describe("ComboBoxItemCustom visual", () => { + it("basic state", () => { + cy.mount( + + + 🇩🇿 Algeria + + + 🇧🇬 Bulgaria + + + 🇨🇦 Canada + + + ); + cy.screenshot(); + }); + + it("dropdown open", () => { + cy.mount( + + + 🇩🇿 Algeria + + + 🇧🇬 Bulgaria + + + 🇨🇦 Canada + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("dropdown open — first item focused via arrow key", () => { + cy.mount( + + + 🇩🇿 Algeria + + + 🇧🇬 Bulgaria + + + 🇨🇦 Canada + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.realPress("ArrowDown"); + cy.screenshot(); + }); + + it("filtering — type to narrow list", () => { + cy.mount( + + + 🇩🇿 Algeria + + + 🇧🇬 Bulgaria + + + 🇨🇦 Canada + + + ); + cy.get("[ui5-combobox]").realClick(); + cy.realType("B"); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + 🇩🇿 Algeria + + + 🇧🇬 Bulgaria + + + 🇨🇦 Canada + + +
+ ); + cy.screenshot(); + }); +}); + +describe("ComboBoxItemGroup visual", () => { + it("basic state", () => { + cy.mount( + + + + + + + + + + + ); + cy.screenshot(); + }); + + it("dropdown open", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("dropdown open — first item focused via arrow key", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.realPress("ArrowDown"); + cy.screenshot(); + }); + + it("dropdown open — group header focused via arrow key", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-combobox]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.realPress("ArrowDown"); + cy.realPress("ArrowDown"); + cy.realPress("ArrowDown"); + cy.screenshot(); + }); + + it("filtering — type to narrow list", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-combobox]").realClick(); + cy.realType("B"); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + + + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/DatePicker.cy.tsx b/packages/main/cypress/specs/visuals/DatePicker.cy.tsx index d5193546b091..3680bc2550b9 100644 --- a/packages/main/cypress/specs/visuals/DatePicker.cy.tsx +++ b/packages/main/cypress/specs/visuals/DatePicker.cy.tsx @@ -6,6 +6,7 @@ const FIXED_VALUE = "Jan 15, 2024"; describe("DatePicker visual", () => { beforeEach(() => { + cy.clock(new Date("Jan 15, 2024").getTime(), ["Date"]); setAnimationMode(AnimationMode.None); }); @@ -38,6 +39,26 @@ describe("DatePicker visual", () => { cy.screenshot(); }); + it("value state — Negative — calendar open", () => { + cy.mount( + + Invalid date + + ); + cy.get("[ui5-date-picker]").ui5DatePickerValueHelpIconPress(); + cy.screenshot(); + }); + + it("value state — Negative — focused", () => { + cy.mount( + + Invalid date + + ); + cy.get("[ui5-date-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Critical", () => { cy.mount( @@ -47,6 +68,26 @@ describe("DatePicker visual", () => { cy.screenshot(); }); + it("value state — Critical — calendar open", () => { + cy.mount( + + Date outside range + + ); + cy.get("[ui5-date-picker]").ui5DatePickerValueHelpIconPress(); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount( + + Date outside range + + ); + cy.get("[ui5-date-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Positive", () => { cy.mount( @@ -56,6 +97,26 @@ describe("DatePicker visual", () => { cy.screenshot(); }); + it("value state — Positive — calendar open", () => { + cy.mount( + + Valid date + + ); + cy.get("[ui5-date-picker]").ui5DatePickerValueHelpIconPress(); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount( + + Valid date + + ); + cy.get("[ui5-date-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Information", () => { cy.mount( @@ -65,6 +126,26 @@ describe("DatePicker visual", () => { cy.screenshot(); }); + it("value state — Information — calendar open", () => { + cy.mount( + + Date will be used as reference + + ); + cy.get("[ui5-date-picker]").ui5DatePickerValueHelpIconPress(); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount( + + Date will be used as reference + + ); + cy.get("[ui5-date-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("calendar popover open", () => { cy.mount(); cy.get("[ui5-date-picker]").ui5DatePickerValueHelpIconPress(); diff --git a/packages/main/cypress/specs/visuals/DateRangePicker.cy.tsx b/packages/main/cypress/specs/visuals/DateRangePicker.cy.tsx new file mode 100644 index 000000000000..f46f4afa824a --- /dev/null +++ b/packages/main/cypress/specs/visuals/DateRangePicker.cy.tsx @@ -0,0 +1,146 @@ +import DateRangePicker from "../../../src/DateRangePicker.js"; + +describe("DateRangePicker visual", () => { + beforeEach(() => { + cy.clock(new Date("Jan 15, 2024").getTime(), ["Date"]); + }); + + it("basic state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); + + it("calendar open", () => { + cy.mount(); + cy.get("[ui5-daterange-picker]").ui5DatePickerValueHelpIconPress(); + cy.get("[ui5-daterange-picker]").ui5DateRangePickerExpectToBeOpen(); + cy.screenshot(); + }); + + it("calendar open — two months", () => { + cy.mount(); + cy.get("[ui5-daterange-picker]").ui5DatePickerValueHelpIconPress(); + cy.get("[ui5-daterange-picker]").ui5DateRangePickerExpectToBeOpen(); + cy.screenshot(); + }); + + it("value state — Negative", () => { + cy.mount( + Invalid date range + ); + cy.screenshot(); + }); + + it("value state — Negative — calendar open", () => { + cy.mount( + Invalid date range + ); + cy.get("[ui5-daterange-picker]").ui5DatePickerValueHelpIconPress(); + cy.get("[ui5-daterange-picker]").ui5DateRangePickerExpectToBeOpen(); + cy.screenshot(); + }); + + it("value state — Negative — focused", () => { + cy.mount( + Invalid date range + ); + cy.get("[ui5-daterange-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount( + Date range outside allowed period + ); + cy.screenshot(); + }); + + it("value state — Critical — calendar open", () => { + cy.mount( + Date range outside allowed period + ); + cy.get("[ui5-daterange-picker]").ui5DatePickerValueHelpIconPress(); + cy.get("[ui5-daterange-picker]").ui5DateRangePickerExpectToBeOpen(); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount( + Date range outside allowed period + ); + cy.get("[ui5-daterange-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + + it("value state — Positive", () => { + cy.mount( + Date range is valid + ); + cy.screenshot(); + }); + + it("value state — Positive — calendar open", () => { + cy.mount( + Date range is valid + ); + cy.get("[ui5-daterange-picker]").ui5DatePickerValueHelpIconPress(); + cy.get("[ui5-daterange-picker]").ui5DateRangePickerExpectToBeOpen(); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount( + Date range is valid + ); + cy.get("[ui5-daterange-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + + it("value state — Information", () => { + cy.mount( + Select a date range + ); + cy.screenshot(); + }); + + it("value state — Information — calendar open", () => { + cy.mount( + Select a date range + ); + cy.get("[ui5-daterange-picker]").ui5DatePickerValueHelpIconPress(); + cy.get("[ui5-daterange-picker]").ui5DateRangePickerExpectToBeOpen(); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount( + Select a date range + ); + cy.get("[ui5-daterange-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + + it("disabled state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("readonly state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("placeholder", () => { + cy.mount(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/DateTimePicker.cy.tsx b/packages/main/cypress/specs/visuals/DateTimePicker.cy.tsx index ba9892c9d368..e1bcdcd9c9b7 100644 --- a/packages/main/cypress/specs/visuals/DateTimePicker.cy.tsx +++ b/packages/main/cypress/specs/visuals/DateTimePicker.cy.tsx @@ -7,6 +7,7 @@ const FIXED_FORMAT = "MMM d, yyyy, hh:mm:ss a"; describe("DateTimePicker visual", () => { beforeEach(() => { + cy.clock(new Date("Apr 15, 2024").getTime(), ["Date"]); setAnimationMode(AnimationMode.None); }); @@ -39,6 +40,26 @@ describe("DateTimePicker visual", () => { cy.screenshot(); }); + it("value state — Negative — popover open", () => { + cy.mount( + + Invalid date and time + + ); + cy.get("[ui5-datetime-picker]").ui5DateTimePickerOpen(); + cy.screenshot(); + }); + + it("value state — Negative — focused", () => { + cy.mount( + + Invalid date and time + + ); + cy.get("[ui5-datetime-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Critical", () => { cy.mount( @@ -48,6 +69,26 @@ describe("DateTimePicker visual", () => { cy.screenshot(); }); + it("value state — Critical — popover open", () => { + cy.mount( + + Date outside allowed range + + ); + cy.get("[ui5-datetime-picker]").ui5DateTimePickerOpen(); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount( + + Date outside allowed range + + ); + cy.get("[ui5-datetime-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Positive", () => { cy.mount( @@ -57,6 +98,26 @@ describe("DateTimePicker visual", () => { cy.screenshot(); }); + it("value state — Positive — popover open", () => { + cy.mount( + + Date is valid + + ); + cy.get("[ui5-datetime-picker]").ui5DateTimePickerOpen(); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount( + + Date is valid + + ); + cy.get("[ui5-datetime-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Information", () => { cy.mount( @@ -66,6 +127,26 @@ describe("DateTimePicker visual", () => { cy.screenshot(); }); + it("value state — Information — popover open", () => { + cy.mount( + + Date will be used as reference + + ); + cy.get("[ui5-datetime-picker]").ui5DateTimePickerOpen(); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount( + + Date will be used as reference + + ); + cy.get("[ui5-datetime-picker]").shadow().find("ui5-datetime-input").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("popover open", () => { cy.mount(); cy.get("[ui5-datetime-picker]").ui5DateTimePickerOpen(); diff --git a/packages/main/cypress/specs/visuals/Dialog.cy.tsx b/packages/main/cypress/specs/visuals/Dialog.cy.tsx new file mode 100644 index 000000000000..acc79be7404c --- /dev/null +++ b/packages/main/cypress/specs/visuals/Dialog.cy.tsx @@ -0,0 +1,114 @@ +import Dialog from "../../../src/Dialog.js"; +import Button from "../../../src/Button.js"; +import Bar from "../../../src/Bar.js"; +import Title from "../../../src/Title.js"; + +const openDialog = (id: string) => { + cy.get(`[ui5-dialog]#${id}`).invoke("prop", "open", true); + cy.get(`[ui5-dialog]#${id}`).ui5DialogOpened(); +}; + +describe("Dialog visual", () => { + it("basic state — open with header text and content", () => { + cy.mount( + +

Are you sure you want to proceed?

+ + +
+ ); + openDialog("dialog1"); + cy.screenshot(); + }); + + it("with custom header slot", () => { + cy.mount( + + + Custom Header + +

Dialog content goes here.

+ +
+ ); + openDialog("dialog2"); + cy.screenshot(); + }); + + it("state — Negative", () => { + cy.mount( + +

Something went wrong.

+ +
+ ); + openDialog("dialog3"); + cy.screenshot(); + }); + + it("state — Critical", () => { + cy.mount( + +

This action may have consequences.

+ +
+ ); + openDialog("dialog4"); + cy.screenshot(); + }); + + it("state — Positive", () => { + cy.mount( + +

Operation completed successfully.

+ +
+ ); + openDialog("dialog5"); + cy.screenshot(); + }); + + it("state — Information", () => { + cy.mount( + +

Please read the following information.

+ +
+ ); + openDialog("dialog6"); + cy.screenshot(); + }); + + it("stretch mode", () => { + cy.mount( + +

This dialog stretches to fill the viewport.

+ +
+ ); + openDialog("dialog7"); + cy.screenshot(); + }); + + it("draggable", () => { + cy.mount( + +

This dialog can be dragged.

+ +
+ ); + openDialog("dialog8"); + cy.screenshot(); + }); + + it("resizable", () => { + cy.mount( + +

This dialog can be resized.

+ +
+ ); + openDialog("dialog9"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/DynamicDateRange.cy.tsx b/packages/main/cypress/specs/visuals/DynamicDateRange.cy.tsx new file mode 100644 index 000000000000..752b850204c1 --- /dev/null +++ b/packages/main/cypress/specs/visuals/DynamicDateRange.cy.tsx @@ -0,0 +1,82 @@ +import DynamicDateRange from "../../../src/DynamicDateRange.js"; +import "../../../src/dynamic-date-range-options/Today.js"; +import "../../../src/dynamic-date-range-options/Yesterday.js"; +import "../../../src/dynamic-date-range-options/Tomorrow.js"; +import "../../../src/dynamic-date-range-options/SingleDate.js"; +import "../../../src/dynamic-date-range-options/DateRange.js"; +import "../../../src/dynamic-date-range-options/DateTimeRange.js"; +import "../../../src/dynamic-date-range-options/FromDateTime.js"; +import "../../../src/dynamic-date-range-options/ToDateTime.js"; +import "../../../src/dynamic-date-range-options/LastOptions.js"; +import "../../../src/dynamic-date-range-options/NextOptions.js"; + +describe("DynamicDateRange visual", () => { + beforeEach(() => { + cy.clock(new Date("Jan 15, 2024").getTime(), ["Date"]); + }); + + it("basic state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); + + it("with value — Today", () => { + cy.mount(); + cy.screenshot(); + }); + + it("popover open — options list", () => { + cy.mount(); + cy.get("[ui5-dynamic-date-range]").ui5DynamicDateRangeOpen(); + cy.screenshot(); + }); + + it("popover open — Date option selected (calendar shown)", () => { + cy.mount(); + cy.get("[ui5-dynamic-date-range]") + .ui5DynamicDateRangeOpen() + .ui5DynamicDateRangeGetOptionsList() + .contains("Date") + .realClick(); + cy.get("[ui5-dynamic-date-range]") + .shadow() + .find("[ui5-responsive-popover]") + .ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("popover open — Last X option selected (step input shown)", () => { + cy.mount(); + cy.get("[ui5-dynamic-date-range]") + .ui5DynamicDateRangeOpen() + .ui5DynamicDateRangeGetOptionsList() + .first() + .realClick(); + cy.get("[ui5-dynamic-date-range]") + .shadow() + .find("[ui5-responsive-popover]") + .ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("popover open — DateTimeRange option selected", () => { + cy.mount(); + cy.get("[ui5-dynamic-date-range]") + .ui5DynamicDateRangeOpen() + .ui5DynamicDateRangeSelectOption(); + cy.get("[ui5-dynamic-date-range]") + .shadow() + .find("[ui5-responsive-popover]") + .ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ExpandableText.cy.tsx b/packages/main/cypress/specs/visuals/ExpandableText.cy.tsx new file mode 100644 index 000000000000..cc2a8c77b77b --- /dev/null +++ b/packages/main/cypress/specs/visuals/ExpandableText.cy.tsx @@ -0,0 +1,68 @@ +import ExpandableText from "../../../src/ExpandableText.js"; + +const SHORT_TEXT = "Short text that fits within limit."; +const LONG_TEXT = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."; + +describe("ExpandableText visual", () => { + it("text within maxCharacters — no toggle shown", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("text exceeds maxCharacters — collapsed (Show More)", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("text exceeds maxCharacters — expanded (Show Less)", () => { + cy.mount( + + ); + cy.get("[ui5-expandable-text]").shadow().find(".ui5-exp-text-toggle").realClick(); + cy.screenshot(); + }); + + it("overflow mode — Popover (collapsed)", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("overflow mode — Popover (open)", () => { + cy.mount( + + ); + cy.get("[ui5-expandable-text]").shadow().find(".ui5-exp-text-toggle").realClick(); + cy.get("[ui5-expandable-text]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("empty indicator mode — On", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("custom maxCharacters — 50", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/FileUploader.cy.tsx b/packages/main/cypress/specs/visuals/FileUploader.cy.tsx new file mode 100644 index 000000000000..a0c5a4189b16 --- /dev/null +++ b/packages/main/cypress/specs/visuals/FileUploader.cy.tsx @@ -0,0 +1,90 @@ +import FileUploader from "../../../src/FileUploader.js"; +import Button from "../../../src/Button.js"; +import Label from "../../../src/Label.js"; + +describe("FileUploader visual", () => { + it("basic state", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("with value — file name shown", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("value state — Negative", () => { + cy.mount( + + Invalid file type. + + ); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount( + + File exceeds size limit. + + ); + cy.screenshot(); + }); + + it("value state — Positive", () => { + cy.mount( + + File is valid. + + ); + cy.screenshot(); + }); + + it("value state — Information", () => { + cy.mount( + + Accepted formats: PDF, DOCX. + + ); + cy.screenshot(); + }); + + it("hide-input — custom button trigger", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("with label", () => { + cy.mount( +
+ + +
+ ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Form.cy.tsx b/packages/main/cypress/specs/visuals/Form.cy.tsx new file mode 100644 index 000000000000..90fe5cffa394 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Form.cy.tsx @@ -0,0 +1,235 @@ +import Form from "../../../src/Form.js"; +import FormGroup from "../../../src/FormGroup.js"; +import FormItem from "../../../src/FormItem.js"; +import Label from "../../../src/Label.js"; +import Text from "../../../src/Text.js"; +import Input from "../../../src/Input.js"; + +describe("Form visual", () => { + it("basic state — with header and grouped items", () => { + cy.mount( +
+ + + + Red Point Stores + + + + 411 Maintown + + + + + + + @sap + + + + john.smith@sap.com + + +
+ ); + cy.screenshot(); + }); + + it("without groups — flat form items", () => { + cy.mount( +
+ + + Red Point Stores + + + + Main St 1618 + + + + Germany + +
+ ); + cy.screenshot(); + }); + + it("without header text", () => { + cy.mount( +
+ + + + Red Point Stores + + +
+ ); + cy.screenshot(); + }); + + it("edit mode — inputs instead of text", () => { + cy.mount( +
+ + + + + + + + + + + + + + + + + + + + + + +
+ ); + cy.screenshot(); + }); + + it("labels on top — labelSpan S12 M12 L12 XL12", () => { + cy.mount( +
+ + + Red Point Stores + + + + 411 Maintown + + + + Main St 1618 + + + + Germany + +
+ ); + cy.screenshot(); + }); + + it("multi-column layout — S1 M2 L3 XL4", () => { + cy.mount( +
+ + + + Red Point Stores + + + + + + + @sap + + + + john.smith@sap.com + + + + + + + www.sap.com + + +
+ ); + cy.screenshot(); + }); + + it("FormGroup without header text", () => { + cy.mount( +
+ + + + Red Point Stores + + + + Germany + + +
+ ); + cy.screenshot(); + }); + + it("item-spacing Large", () => { + cy.mount( +
+ + + + Red Point Stores + + + + Germany + + +
+ ); + cy.screenshot(); + }); + + it("custom header slot", () => { + cy.mount( +
+
Custom Header
+ + + + Red Point Stores + + +
+ ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+
+ + + + Red Point Stores + + + + Germany + + + + + + + john.smith@sap.com + + +
+
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Icon.cy.tsx b/packages/main/cypress/specs/visuals/Icon.cy.tsx new file mode 100644 index 000000000000..3d07c0e99bf0 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Icon.cy.tsx @@ -0,0 +1,92 @@ +import Icon from "../../../src/Icon.js"; +import addEquipment from "@ui5/webcomponents-icons/dist/add-equipment.js"; +import save from "@ui5/webcomponents-icons/dist/save.js"; +import error from "@ui5/webcomponents-icons/dist/error.js"; +import add from "@ui5/webcomponents-icons/dist/add.js"; + +describe("Icon visual", () => { + it("basic state — SVG icon", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("interactive mode", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("interactive mode — focused", () => { + cy.mount( + + ); + cy.get("[ui5-icon]").shadow().find(".ui5-icon-root").focus(); + cy.screenshot(); + }); + + it("image mode with accessible name", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("with tooltip", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("custom size via style", () => { + cy.mount( +
+ + + +
+ ); + cy.screenshot(); + }); + + it("fontIcon slot — basic", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("fontIcon slot — interactive mode", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("fontIcon slot — interactive mode focused", () => { + cy.mount( + + + + ); + cy.get("[ui5-icon]").shadow().find("span.ui5-icon-root").focus(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Input.cy.tsx b/packages/main/cypress/specs/visuals/Input.cy.tsx index 5d3eae985634..7539f98fefc4 100644 --- a/packages/main/cypress/specs/visuals/Input.cy.tsx +++ b/packages/main/cypress/specs/visuals/Input.cy.tsx @@ -45,6 +45,16 @@ describe("Input visual", () => { cy.screenshot(); }); + it("value state — Negative — focused", () => { + cy.mount( + + Invalid input + + ); + cy.get("[ui5-input]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Critical", () => { cy.mount( @@ -54,6 +64,16 @@ describe("Input visual", () => { cy.screenshot(); }); + it("value state — Critical — focused", () => { + cy.mount( + + Please review + + ); + cy.get("[ui5-input]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Positive", () => { cy.mount( @@ -63,6 +83,16 @@ describe("Input visual", () => { cy.screenshot(); }); + it("value state — Positive — focused", () => { + cy.mount( + + Looks good + + ); + cy.get("[ui5-input]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Information", () => { cy.mount( @@ -72,6 +102,16 @@ describe("Input visual", () => { cy.screenshot(); }); + it("value state — Information — focused", () => { + cy.mount( + + Additional info + + ); + cy.get("[ui5-input]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("compact mode", () => { cy.mount(
diff --git a/packages/main/cypress/specs/visuals/Label.cy.tsx b/packages/main/cypress/specs/visuals/Label.cy.tsx new file mode 100644 index 000000000000..1af38b95f705 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Label.cy.tsx @@ -0,0 +1,50 @@ +import Label from "../../../src/Label.js"; + +describe("Label visual", () => { + it("basic state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); + + it("required", () => { + cy.mount(); + cy.screenshot(); + }); + + it("show-colon", () => { + cy.mount(); + cy.screenshot(); + }); + + it("required and show-colon", () => { + cy.mount(); + cy.screenshot(); + }); + + it("wrapping — long text", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("truncated — wrappingType None", () => { + cy.mount( + + ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Link.cy.tsx b/packages/main/cypress/specs/visuals/Link.cy.tsx new file mode 100644 index 000000000000..d13ca437e4aa --- /dev/null +++ b/packages/main/cypress/specs/visuals/Link.cy.tsx @@ -0,0 +1,66 @@ +import Link from "../../../src/Link.js"; + +describe("Link visual", () => { + it("basic state", () => { + cy.mount(Default Link); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ Default Link +
+ ); + cy.screenshot(); + }); + + it("design — Subtle", () => { + cy.mount(Subtle Link); + cy.screenshot(); + }); + + it("design — Emphasized", () => { + cy.mount(Emphasized Link); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount(Disabled Link); + cy.screenshot(); + }); + + it("with icon", () => { + cy.mount(View employee profile); + cy.screenshot(); + }); + + it("with end icon", () => { + cy.mount(Weather today); + cy.screenshot(); + }); + + it("wrapping — long text", () => { + cy.mount( + + Eu enim consectetur do amet elit Lorem ipsum dolor sit amet consectetur. + + ); + cy.screenshot(); + }); + + it("truncated — wrappingType None", () => { + cy.mount( + + Eu enim consectetur do amet elit Lorem ipsum dolor sit amet consectetur. + + ); + cy.screenshot(); + }); + + it("focused", () => { + cy.mount(Focused Link); + cy.get("[ui5-link]").focus(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/List.cy.tsx b/packages/main/cypress/specs/visuals/List.cy.tsx new file mode 100644 index 000000000000..3e37d2c0bc8a --- /dev/null +++ b/packages/main/cypress/specs/visuals/List.cy.tsx @@ -0,0 +1,132 @@ +import List from "../../../src/List.js"; +import ListItemStandard from "../../../src/ListItemStandard.js"; +import ListItemGroup from "../../../src/ListItemGroup.js"; + +describe("List visual", () => { + it("basic state", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + Keyboard Logitech + + ); + cy.screenshot(); + }); + + it("no header", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("selection mode — Single", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("selection mode — Multiple", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + Keyboard Logitech + + ); + cy.screenshot(); + }); + + it("selection mode — Delete", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("with groups", () => { + cy.mount( + + + Laptop Lenovo + HP Monitor 24 + + + iPhone 14 + Samsung Galaxy + + + ); + cy.screenshot(); + }); + + it("separators — None", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("separators — Inner", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("growing — Button", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("no data text", () => { + cy.mount( + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + Laptop Lenovo + iPhone 14 + HP Monitor 24 + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ListItemGroup.cy.tsx b/packages/main/cypress/specs/visuals/ListItemGroup.cy.tsx new file mode 100644 index 000000000000..2ba6da1205c6 --- /dev/null +++ b/packages/main/cypress/specs/visuals/ListItemGroup.cy.tsx @@ -0,0 +1,85 @@ +import List from "../../../src/List.js"; +import ListItemStandard from "../../../src/ListItemStandard.js"; +import ListItemGroup from "../../../src/ListItemGroup.js"; + +describe("ListItemGroup visual", () => { + it("basic state — with header text", () => { + cy.mount( + + + Laptop Lenovo + HP Monitor 24 + Keyboard Logitech + + + ); + cy.screenshot(); + }); + + it("without header text", () => { + cy.mount( + + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + + ); + cy.screenshot(); + }); + + it("multiple groups", () => { + cy.mount( + + + Laptop Lenovo + HP Monitor 24 + + + iPhone 14 + Samsung Galaxy + + + Keyboard Logitech + Mouse MX Master + + + ); + cy.screenshot(); + }); + + it("with selection mode — Multiple", () => { + cy.mount( + + + Laptop Lenovo + HP Monitor 24 + + + iPhone 14 + Samsung Galaxy + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + Laptop Lenovo + HP Monitor 24 + + + iPhone 14 + Samsung Galaxy + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ListItemStandard.cy.tsx b/packages/main/cypress/specs/visuals/ListItemStandard.cy.tsx new file mode 100644 index 000000000000..56f7523f816c --- /dev/null +++ b/packages/main/cypress/specs/visuals/ListItemStandard.cy.tsx @@ -0,0 +1,142 @@ +import List from "../../../src/List.js"; +import ListItemStandard from "../../../src/ListItemStandard.js"; +import Avatar from "../../../src/Avatar.js"; + +describe("ListItemStandard visual", () => { + it("basic state", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("with description", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("with icon", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("with image — avatar slot", () => { + cy.mount( + + + + John Doe + + + + Jane Smith + + + ); + cy.screenshot(); + }); + + it("type — Active", () => { + cy.mount( + + Active Item + Inactive Item + Navigation Item + + ); + cy.screenshot(); + }); + + it("type — Detail", () => { + cy.mount( + + Detail Item 1 + Detail Item 2 + + ); + cy.screenshot(); + }); + + it("selected state — Single mode", () => { + cy.mount( + + Selected Item + Unselected Item + Another Item + + ); + cy.screenshot(); + }); + + it("selected state — Multiple mode", () => { + cy.mount( + + First Selected + Second Selected + Unselected Item + + ); + cy.screenshot(); + }); + + it("navigated state", () => { + cy.mount( + + Navigated Item + Regular Item + Regular Item + + ); + cy.screenshot(); + }); + + it("icon end", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + + ); + cy.screenshot(); + }); + + it("delete mode", () => { + cy.mount( + + Laptop Lenovo + iPhone 14 + HP Monitor 24 + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + Laptop Lenovo + iPhone 14 + HP Monitor 24 + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Menu.cy.tsx b/packages/main/cypress/specs/visuals/Menu.cy.tsx new file mode 100644 index 000000000000..9b50553c7ce4 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Menu.cy.tsx @@ -0,0 +1,220 @@ +import Button from "../../../src/Button.js"; +import Menu from "../../../src/Menu.js"; +import MenuItem from "../../../src/MenuItem.js"; +import MenuItemGroup from "../../../src/MenuItemGroup.js"; +import MenuSeparator from "../../../src/MenuSeparator.js"; + +describe("Menu visual", () => { + it("basic state — open with simple items", () => { + cy.mount( + <> + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("items with icons", () => { + cy.mount( + <> + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("items with additional text", () => { + cy.mount( + <> + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("disabled items", () => { + cy.mount( + <> + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("item with endContent", () => { + cy.mount( + <> + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("item with submenu indicator — submenu open via keyboard", () => { + cy.mount( + <> + + + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.get("[ui5-menu] > [ui5-menu-item]").first().should("be.focused").realPress("ArrowRight"); + cy.get("[ui5-menu-item][text='New']").shadow().find("[ui5-responsive-popover]").should("have.attr", "open"); + cy.screenshot(); + }); + + it("checked item (outside group)", () => { + cy.mount( + <> + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("MenuSeparator — single separator", () => { + cy.mount( + <> + + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("MenuSeparator — multiple separators", () => { + cy.mount( + <> + + + + + + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("MenuItemGroup — checkMode Single", () => { + cy.mount( + <> + + + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("MenuItemGroup — checkMode Multiple", () => { + cy.mount( + <> + + + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); + + it("mixed groups and separators", () => { + cy.mount( + <> + + + + + + + + + + + + + + + + ); + cy.get("[ui5-menu]").ui5MenuOpen(); + cy.get("[ui5-menu]").ui5MenuOpened(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/MultiComboBox.cy.tsx b/packages/main/cypress/specs/visuals/MultiComboBox.cy.tsx index b53ab8cddb21..13d0b39c9ef7 100644 --- a/packages/main/cypress/specs/visuals/MultiComboBox.cy.tsx +++ b/packages/main/cypress/specs/visuals/MultiComboBox.cy.tsx @@ -1,5 +1,6 @@ import MultiComboBox from "../../../src/MultiComboBox.js"; import MultiComboBoxItem from "../../../src/MultiComboBoxItem.js"; +import MultiComboBoxItemCustom from "../../../src/MultiComboBoxItemCustom.js"; import MultiComboBoxItemGroup from "../../../src/MultiComboBoxItemGroup.js"; describe("MultiComboBox visual", () => { @@ -44,6 +45,19 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); + it("dropdown open — pre-selected items checked", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + it("dropdown open — first item focused via arrow key", () => { cy.mount( @@ -73,17 +87,23 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); - it("with grouped items — dropdown open", () => { + it("value state — Negative", () => { cy.mount( - - - - - - - - - + + Invalid selection + + + + ); + cy.screenshot(); + }); + + it("value state — Negative — dropdown open", () => { + cy.mount( + + Invalid selection + + ); cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); @@ -91,7 +111,7 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); - it("value state — Negative", () => { + it("value state — Negative — focused", () => { cy.mount( Invalid selection @@ -99,6 +119,7 @@ describe("MultiComboBox visual", () => { ); + cy.get("[ui5-multi-combobox]").shadow().find("input").realClick(); cy.screenshot(); }); @@ -113,6 +134,31 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); + it("value state — Critical — dropdown open", () => { + cy.mount( + + Please review your selection + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount( + + Please review your selection + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Positive", () => { cy.mount( @@ -124,6 +170,31 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); + it("value state — Positive — dropdown open", () => { + cy.mount( + + Selection is valid + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount( + + Selection is valid + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("value state — Information", () => { cy.mount( @@ -135,6 +206,31 @@ describe("MultiComboBox visual", () => { cy.screenshot(); }); + it("value state — Information — dropdown open", () => { + cy.mount( + + Select up to 3 items + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount( + + Select up to 3 items + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("input").realClick(); + cy.screenshot(); + }); + it("disabled state", () => { cy.mount( @@ -173,4 +269,150 @@ describe("MultiComboBox visual", () => { ); cy.screenshot(); }); + + // MultiComboBoxItemCustom + it("custom items — basic state", () => { + cy.mount( + + + Algeria + + + Bulgaria + + + Canada + + + ); + cy.screenshot(); + }); + + it("custom items — dropdown open", () => { + cy.mount( + + + Algeria + + + Bulgaria + + + Canada + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("custom items — dropdown open with pre-selected items checked", () => { + cy.mount( + + + Algeria + + + Bulgaria + + + Canada + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + // MultiComboBoxItemGroup + it("grouped items — basic state", () => { + cy.mount( + + + + + + + + + + + ); + cy.screenshot(); + }); + + it("grouped items — dropdown open", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("grouped items — dropdown open with pre-selected items checked", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("grouped items — first item focused via arrow key", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-multi-combobox]").shadow().find("[ui5-icon]").realClick(); + cy.get("[ui5-multi-combobox]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.realPress("ArrowDown"); + cy.screenshot(); + }); + + it("grouped items — filtering", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-multi-combobox]").realClick(); + cy.realType("B"); + cy.screenshot(); + }); }); diff --git a/packages/main/cypress/specs/visuals/MultiInput.cy.tsx b/packages/main/cypress/specs/visuals/MultiInput.cy.tsx new file mode 100644 index 000000000000..6f2cc06e42db --- /dev/null +++ b/packages/main/cypress/specs/visuals/MultiInput.cy.tsx @@ -0,0 +1,183 @@ +import MultiInput from "../../../src/MultiInput.js"; +import Token from "../../../src/Token.js"; +import SuggestionItem from "../../../src/SuggestionItem.js"; + +describe("MultiInput visual", () => { + it("basic state — empty", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode — empty", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); + + it("with tokens", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("with tokens — focused/expanded", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-multi-input]").shadow().find("input").realClick(); + cy.get("[ui5-multi-input]").shadow().find("[ui5-tokenizer]").should("have.attr", "expanded"); + cy.screenshot(); + }); + + it("with value help icon", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("readonly", () => { + cy.mount( + + + + + ); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount( + + + + + ); + cy.screenshot(); + }); + + it("value state — Negative", () => { + cy.mount( + + Error message + + + ); + cy.screenshot(); + }); + + it("value state — Negative — focused", () => { + cy.mount( + + Error message + + + ); + cy.get("[ui5-multi-input]").shadow().find("input").realClick(); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount( + + Warning message + + + ); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount( + + Warning message + + + ); + cy.get("[ui5-multi-input]").shadow().find("input").realClick(); + cy.screenshot(); + }); + + it("value state — Positive", () => { + cy.mount( + + Success message + + + ); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount( + + Success message + + + ); + cy.get("[ui5-multi-input]").shadow().find("input").realClick(); + cy.screenshot(); + }); + + it("value state — Information", () => { + cy.mount( + + Info message + + + ); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount( + + Info message + + + ); + cy.get("[ui5-multi-input]").shadow().find("input").realClick(); + cy.screenshot(); + }); + + it("overflowing tokens — n-more indicator", () => { + cy.mount( + + + + + + + + ); + cy.screenshot(); + }); + + it("suggestions dropdown open", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-multi-input]").shadow().find("input").realClick(); + cy.realType("B"); + cy.get("[ui5-multi-input]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Panel.cy.tsx b/packages/main/cypress/specs/visuals/Panel.cy.tsx new file mode 100644 index 000000000000..540d341991c8 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Panel.cy.tsx @@ -0,0 +1,78 @@ +import Panel from "../../../src/Panel.js"; +import Title from "../../../src/Title.js"; +import Label from "../../../src/Label.js"; + +describe("Panel visual", () => { + it("basic state — expanded with headerText", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + +
+ ); + cy.screenshot(); + }); + + it("collapsed state", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("fixed panel — no toggle arrow", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("custom header slot", () => { + cy.mount( + +
+ Custom Header Title +
+ +
+ ); + cy.screenshot(); + }); + + it("custom header — collapsed", () => { + cy.mount( + +
+ Custom Header Title +
+ +
+ ); + cy.screenshot(); + }); + + it("collapsed via header click", () => { + cy.mount( + + + + ); + cy.get("[ui5-panel]").shadow().find(".ui5-panel-header").realClick(); + cy.wait(300); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Popover.cy.tsx b/packages/main/cypress/specs/visuals/Popover.cy.tsx new file mode 100644 index 000000000000..867984599660 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Popover.cy.tsx @@ -0,0 +1,105 @@ +import Button from "../../../src/Button.js"; +import Popover from "../../../src/Popover.js"; +import List from "../../../src/List.js"; +import ListItemStandard from "../../../src/ListItemStandard.js"; + +describe("Popover visual", () => { + it("basic open state", () => { + cy.mount( + <> + + +
Popover content
+
+ + ); + cy.get("[ui5-popover]").ui5PopoverOpened(); + cy.screenshot(); + }); + + it("open with footer slot", () => { + cy.mount( + <> + + +
Popover content
+
+ + +
+
+ + ); + cy.get("[ui5-popover]").ui5PopoverOpened(); + cy.screenshot(); + }); + + it("open with list content", () => { + cy.mount( + <> + + + + Item 1 + Item 2 + Item 3 + + + + ); + cy.get("[ui5-popover]").ui5PopoverOpened(); + cy.screenshot(); + }); + + it("open without arrow (hide-arrow)", () => { + cy.mount( + <> + + +
Popover without arrow
+
+ + ); + cy.get("[ui5-popover]").ui5PopoverOpened(); + cy.screenshot(); + }); + + it("placement — Top", () => { + cy.mount( + <> + + +
Placed above opener
+
+ + ); + cy.get("[ui5-popover]").ui5PopoverOpened(); + cy.screenshot(); + }); + + it("placement — End", () => { + cy.mount( + <> + + +
Placed to the end of opener
+
+ + ); + cy.get("[ui5-popover]").ui5PopoverOpened(); + cy.screenshot(); + }); + + it("resizable popover", () => { + cy.mount( + <> + + +
Resizable content
+
+ + ); + cy.get("[ui5-popover]").ui5PopoverOpened(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ProgressIndicator.cy.tsx b/packages/main/cypress/specs/visuals/ProgressIndicator.cy.tsx new file mode 100644 index 000000000000..e19fd4749aca --- /dev/null +++ b/packages/main/cypress/specs/visuals/ProgressIndicator.cy.tsx @@ -0,0 +1,72 @@ +import ProgressIndicator from "../../../src/ProgressIndicator.js"; +import { setAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js"; + +describe("ProgressIndicator visual", () => { + before(() => { + cy.wrap({ setAnimationMode }).then(api => api.setAnimationMode("none")); + }); + + it("0% — empty", () => { + cy.mount(); + cy.screenshot(); + }); + + it("25%", () => { + cy.mount(); + cy.screenshot(); + }); + + it("50%", () => { + cy.mount(); + cy.screenshot(); + }); + + it("75%", () => { + cy.mount(); + cy.screenshot(); + }); + + it("100% — full", () => { + cy.mount(); + cy.screenshot(); + }); + + it("custom displayValue", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Negative", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Positive", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Information", () => { + cy.mount(); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/RangeSlider.cy.tsx b/packages/main/cypress/specs/visuals/RangeSlider.cy.tsx new file mode 100644 index 000000000000..8f865b0a6036 --- /dev/null +++ b/packages/main/cypress/specs/visuals/RangeSlider.cy.tsx @@ -0,0 +1,52 @@ +import RangeSlider from "../../../src/RangeSlider.js"; + +describe("RangeSlider visual", () => { + beforeEach(() => { + cy.get("[data-cy-root]").invoke("css", "padding", "50px 100px"); + }); + + it("basic state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); + + it("range in the middle", () => { + cy.mount(); + cy.screenshot(); + }); + + it("narrow range", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with tickmarks", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with tickmarks and label interval", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with tooltip", () => { + cy.mount(); + cy.get("[ui5-range-slider]").shadow().find("[ui5-slider-handle][handle-type='Start']").realClick(); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/RatingIndicator.cy.tsx b/packages/main/cypress/specs/visuals/RatingIndicator.cy.tsx new file mode 100644 index 000000000000..2a71f4e521d9 --- /dev/null +++ b/packages/main/cypress/specs/visuals/RatingIndicator.cy.tsx @@ -0,0 +1,69 @@ +import RatingIndicator from "../../../src/RatingIndicator.js"; +import heart from "@ui5/webcomponents-icons/dist/heart.js"; +import heart2 from "@ui5/webcomponents-icons/dist/heart-2.js"; + +describe("RatingIndicator visual", () => { + it("basic state — no value", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with value", () => { + cy.mount(); + cy.screenshot(); + }); + + it("half-star value", () => { + cy.mount(); + cy.screenshot(); + }); + + it("readonly", () => { + cy.mount(); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount(); + cy.screenshot(); + }); + + it("required", () => { + cy.mount(); + cy.screenshot(); + }); + + it("custom max", () => { + cy.mount(); + cy.screenshot(); + }); + + it("size S", () => { + cy.mount(); + cy.screenshot(); + }); + + it("size L", () => { + cy.mount(); + cy.screenshot(); + }); + + it("custom icons", () => { + cy.mount(); + cy.screenshot(); + }); + + it("custom icons — half-star", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ResponsivePopover.cy.tsx b/packages/main/cypress/specs/visuals/ResponsivePopover.cy.tsx new file mode 100644 index 000000000000..e83c9ddf4261 --- /dev/null +++ b/packages/main/cypress/specs/visuals/ResponsivePopover.cy.tsx @@ -0,0 +1,80 @@ +import Button from "../../../src/Button.js"; +import ResponsivePopover from "../../../src/ResponsivePopover.js"; + +describe("ResponsivePopover visual", () => { + it("basic state — open with content", () => { + cy.mount( + <> + + +
Popover content
+
+ + ); + cy.get("[ui5-responsive-popover]").invoke("attr", "open", true); + cy.get("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("with header and footer slots", () => { + cy.mount( + <> + + +
Popover Header
+
Popover content goes here.
+
+ + +
+
+ + ); + cy.get("[ui5-responsive-popover]").invoke("attr", "open", true); + cy.get("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("contentOnlyOnDesktop — no header/footer on desktop", () => { + cy.mount( + <> + + +
Header
+
Content only on desktop — header hidden.
+
+ + ); + cy.get("[ui5-responsive-popover]").invoke("attr", "open", true); + cy.get("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("placement — End", () => { + cy.mount( +
+ + +
Popover placed at End
+
+
+ ); + cy.get("[ui5-responsive-popover]").invoke("attr", "open", true); + cy.get("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("placement — Top", () => { + cy.mount( +
+ + +
Popover placed at Top
+
+
+ ); + cy.get("[ui5-responsive-popover]").invoke("attr", "open", true); + cy.get("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/SegmentedButton.cy.tsx b/packages/main/cypress/specs/visuals/SegmentedButton.cy.tsx new file mode 100644 index 000000000000..cc4ec5ccaf9d --- /dev/null +++ b/packages/main/cypress/specs/visuals/SegmentedButton.cy.tsx @@ -0,0 +1,97 @@ +import SegmentedButton from "../../../src/SegmentedButton.js"; +import SegmentedButtonItem from "../../../src/SegmentedButtonItem.js"; +import list from "@ui5/webcomponents-icons/dist/list.js"; +import grid from "@ui5/webcomponents-icons/dist/grid.js"; +import calendar from "@ui5/webcomponents-icons/dist/calendar.js"; + +describe("SegmentedButton visual", () => { + it("basic — first item selected by default", () => { + cy.mount( + + First + Second + Third + + ); + cy.screenshot(); + }); + + it("second item selected", () => { + cy.mount( + + First + Second + Third + + ); + cy.screenshot(); + }); + + it("multiple selection mode — two items selected", () => { + cy.mount( + + First + Second + Third + + ); + cy.screenshot(); + }); + + it("with disabled item", () => { + cy.mount( + + First + Second + Third + + ); + cy.screenshot(); + }); + + it("itemsFitContent — items sized to their content", () => { + cy.mount( + + Short + Much longer text + Medium + + ); + cy.screenshot(); + }); + + it("with icons", () => { + cy.mount( + + List + Grid + Calendar + + ); + cy.screenshot(); + }); + + it("icon only", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + First + Second + Third + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Select.cy.tsx b/packages/main/cypress/specs/visuals/Select.cy.tsx index e9ef5f955b39..53d3526579ed 100644 --- a/packages/main/cypress/specs/visuals/Select.cy.tsx +++ b/packages/main/cypress/specs/visuals/Select.cy.tsx @@ -1,5 +1,6 @@ import Select from "../../../src/Select.js"; import Option from "../../../src/Option.js"; +import OptionCustom from "../../../src/OptionCustom.js"; describe("Select visual", () => { it("basic state", () => { @@ -70,6 +71,33 @@ describe("Select visual", () => { cy.screenshot(); }); + it("value state — Negative — dropdown open", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Negative — focused, dropdown closed", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.realPress("Escape"); + cy.screenshot(); + }); + it("value state — Critical", () => { cy.mount( + Please review + + + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Critical — focused, dropdown closed", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.realPress("Escape"); + cy.screenshot(); + }); + it("value state — Positive", () => { cy.mount( + Valid selection + + + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Positive — focused, dropdown closed", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.realPress("Escape"); + cy.screenshot(); + }); + it("value state — Information", () => { cy.mount( + Select one option + + + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Information — focused, dropdown closed", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.realPress("Escape"); + cy.screenshot(); + }); + it("compact mode", () => { cy.mount(
@@ -115,4 +224,92 @@ describe("Select visual", () => { ); cy.screenshot(); }); + + // Option + it("option with icon — dropdown open", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("option with additional text — dropdown open", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("disabled option in dropdown", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + // OptionCustom + it("custom options — dropdown open", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("custom options with rich content — dropdown open", () => { + cy.mount( + + ); + cy.get("[ui5-select]").realClick(); + cy.get("[ui5-select]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); }); diff --git a/packages/main/cypress/specs/visuals/Slider.cy.tsx b/packages/main/cypress/specs/visuals/Slider.cy.tsx new file mode 100644 index 000000000000..7d059bf0a50e --- /dev/null +++ b/packages/main/cypress/specs/visuals/Slider.cy.tsx @@ -0,0 +1,52 @@ +import Slider from "../../../src/Slider.js"; + +describe("Slider visual", () => { + beforeEach(() => { + cy.get("[data-cy-root]").invoke("css", "padding", "50px 100px"); + }); + + it("basic state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); + + it("value at 50%", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value at 100%", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with tickmarks", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with tickmarks and label interval", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with tooltip", () => { + cy.mount(); + cy.get("[ui5-slider]").shadow().find("[ui5-slider-handle]").realClick(); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/SpecialCalendarDate.cy.tsx b/packages/main/cypress/specs/visuals/SpecialCalendarDate.cy.tsx new file mode 100644 index 000000000000..822890e55b98 --- /dev/null +++ b/packages/main/cypress/specs/visuals/SpecialCalendarDate.cy.tsx @@ -0,0 +1,36 @@ +import Calendar from "../../../src/Calendar.js"; +import SpecialCalendarDate from "../../../src/SpecialCalendarDate.js"; + +describe("SpecialCalendarDate visual", () => { + it("Working type", () => { + cy.mount( + + + + + ); + cy.screenshot(); + }); + + it("NonWorking type", () => { + cy.mount( + + + + + ); + cy.screenshot(); + }); + + it("multiple types together", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/SplitButton.cy.tsx b/packages/main/cypress/specs/visuals/SplitButton.cy.tsx new file mode 100644 index 000000000000..a562fcdf1f5a --- /dev/null +++ b/packages/main/cypress/specs/visuals/SplitButton.cy.tsx @@ -0,0 +1,66 @@ +import SplitButton from "../../../src/SplitButton.js"; + +describe("SplitButton visual", () => { + it("design — Default", () => { + cy.mount(Default); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ Default +
+ ); + cy.screenshot(); + }); + + it("design — Emphasized", () => { + cy.mount(Emphasized); + cy.screenshot(); + }); + + it("design — Positive", () => { + cy.mount(Positive); + cy.screenshot(); + }); + + it("design — Negative", () => { + cy.mount(Negative); + cy.screenshot(); + }); + + it("design — Transparent", () => { + cy.mount(Transparent); + cy.screenshot(); + }); + + it("design — Attention", () => { + cy.mount(Attention); + cy.screenshot(); + }); + + it("with icon", () => { + cy.mount(Add Item); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount(Disabled); + cy.screenshot(); + }); + + it("all designs in a row", () => { + cy.mount( +
+ Default + Emphasized + Positive + Negative + Transparent + Attention +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/StepInput.cy.tsx b/packages/main/cypress/specs/visuals/StepInput.cy.tsx new file mode 100644 index 000000000000..7c3c5ab448de --- /dev/null +++ b/packages/main/cypress/specs/visuals/StepInput.cy.tsx @@ -0,0 +1,67 @@ +import StepInput from "../../../src/StepInput.js"; + +describe("StepInput visual", () => { + it("basic state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with placeholder", () => { + cy.mount(); + cy.screenshot(); + }); + + it("at minimum value", () => { + cy.mount(); + cy.screenshot(); + }); + + it("at maximum value", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with decimal precision", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Negative", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Positive", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Information", () => { + cy.mount(); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount(); + cy.screenshot(); + }); + + it("readonly", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/SuggestionItem.cy.tsx b/packages/main/cypress/specs/visuals/SuggestionItem.cy.tsx new file mode 100644 index 000000000000..5024660ca5cf --- /dev/null +++ b/packages/main/cypress/specs/visuals/SuggestionItem.cy.tsx @@ -0,0 +1,47 @@ +import Input from "../../../src/Input.js"; +import SuggestionItem from "../../../src/SuggestionItem.js"; + +describe("SuggestionItem visual", () => { + it("suggestions dropdown open", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-input]").realClick(); + cy.realType("i"); + cy.get("[ui5-input]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("with additionalText", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-input]").realClick(); + cy.realType("i"); + cy.get("[ui5-input]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("first item focused via arrow key", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-input]").realClick(); + cy.realType("i"); + cy.get("[ui5-input]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.realPress("ArrowDown"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/SuggestionItemCustom.cy.tsx b/packages/main/cypress/specs/visuals/SuggestionItemCustom.cy.tsx new file mode 100644 index 000000000000..dbb6382bde1e --- /dev/null +++ b/packages/main/cypress/specs/visuals/SuggestionItemCustom.cy.tsx @@ -0,0 +1,33 @@ +import Input from "../../../src/Input.js"; +import SuggestionItemCustom from "../../../src/SuggestionItemCustom.js"; + +describe("SuggestionItemCustom visual", () => { + it("suggestions dropdown open", () => { + cy.mount( + + Item 1 + Item 2 + Item 3 + + ); + cy.get("[ui5-input]").realClick(); + cy.realType("i"); + cy.get("[ui5-input]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("first item focused via arrow key", () => { + cy.mount( + + Item 1 + Item 2 + Item 3 + + ); + cy.get("[ui5-input]").realClick(); + cy.realType("i"); + cy.get("[ui5-input]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.realPress("ArrowDown"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/SuggestionItemGroup.cy.tsx b/packages/main/cypress/specs/visuals/SuggestionItemGroup.cy.tsx new file mode 100644 index 000000000000..93f1c8d5ffef --- /dev/null +++ b/packages/main/cypress/specs/visuals/SuggestionItemGroup.cy.tsx @@ -0,0 +1,46 @@ +import Input from "../../../src/Input.js"; +import SuggestionItem from "../../../src/SuggestionItem.js"; +import SuggestionItemGroup from "../../../src/SuggestionItemGroup.js"; + +describe("SuggestionItemGroup visual", () => { + it("grouped suggestions dropdown open", () => { + cy.mount( + + + + + + + + + + + + + ); + cy.get("[ui5-input]").realClick(); + cy.realType("i"); + cy.get("[ui5-input]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("item focused via arrow key", () => { + cy.mount( + + + + + + + + + + + ); + cy.get("[ui5-input]").realClick(); + cy.realType("i"); + cy.get("[ui5-input]").shadow().find("ui5-responsive-popover").ui5ResponsivePopoverOpened(); + cy.realPress("ArrowDown"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/TabContainer.cy.tsx b/packages/main/cypress/specs/visuals/TabContainer.cy.tsx new file mode 100644 index 000000000000..7a3cf95e6cc4 --- /dev/null +++ b/packages/main/cypress/specs/visuals/TabContainer.cy.tsx @@ -0,0 +1,375 @@ +import TabContainer from "../../../src/TabContainer.js"; +import Tab from "../../../src/Tab.js"; +import TabSeparator from "../../../src/TabSeparator.js"; +import Button from "../../../src/Button.js"; +import "@ui5/webcomponents-icons/dist/employee.js"; +import "@ui5/webcomponents-icons/dist/calendar.js"; +import "@ui5/webcomponents-icons/dist/settings.js"; +import "@ui5/webcomponents-icons/dist/menu2.js"; + +describe("TabContainer visual", () => { + it("basic — first tab selected by default", () => { + cy.mount( + + Products content + Availability content + Reviews content + + ); + cy.screenshot(); + }); + + it("second tab selected", () => { + cy.mount( + + Products content + Availability content + Reviews content + + ); + cy.screenshot(); + }); + + it("with icons", () => { + cy.mount( + + Employees content + Calendar content + Settings content + + ); + cy.screenshot(); + }); + + it("icon-only tabs", () => { + cy.mount( + + Content 1 + Content 2 + Content 3 + + ); + cy.screenshot(); + }); + + it("tab designs — Positive, Negative, Critical", () => { + cy.mount( + + Default content + Positive content + Negative content + Critical content + + ); + cy.screenshot(); + }); + + it("with disabled tab", () => { + cy.mount( + + Active content + Disabled content + Also Active content + + ); + cy.screenshot(); + }); + + it("with additional text", () => { + cy.mount( + + Content + Content + + ); + cy.screenshot(); + }); + + it("overflow — end overflow mode", () => { + cy.mount( + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + + ); + cy.screenshot(); + }); + + it("overflow — end overflow popover open", () => { + cy.mount( + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + + ); + cy.get("[ui5-tabcontainer]").ui5TabContainerOpenEndOverflow(); + cy.screenshot(); + }); + + it("overflow — end overflow popover open with nested tabs", () => { + cy.mount( + + One + + Two One + Two Two + + + + Three One One + + + Four + Five + Six + Seven + + ); + cy.get("[ui5-tabcontainer]").ui5TabContainerOpenEndOverflow(); + cy.screenshot(); + }); + + it("overflow — StartAndEnd mode", () => { + cy.mount( + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + ); + cy.screenshot(); + }); + + it("overflow — StartAndEnd start overflow popover open", () => { + cy.mount( + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + ); + cy.get("#tcStartAndEnd").shadow().find("[data-ui5-stable='overflow-start']").should("be.visible").click(); + cy.get("#tcStartAndEnd").shadow().find(".ui5-tab-container-responsive-popover").should("be.visible"); + cy.screenshot(); + }); + + it("overflow — custom overflow buttons", () => { + cy.mount( + + + + One + Two + Three + Four + Five + Six + Seven + Eight + Nine + Ten + Eleven + Twelve + + ); + cy.screenshot(); + }); + + it("nested tabs — strip with expand arrows", () => { + cy.mount( + + One content + Two content + Two One + Two Two + + Three content + + Three One One + + + Four content + + ); + cy.screenshot(); + }); + + it("nested tabs — sub-tab list open", () => { + cy.mount( + + One content + Two content + Two One + Two Two + + Three content + + Three One One + + + Four content + + ); + cy.get("#tcNested").shadow().find(".ui5-tab-expand-button [ui5-button]").first().realClick(); + cy.get("#tcNested").shadow().find(".ui5-tab-container-responsive-popover").should("be.visible"); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + Products content + Availability content + Reviews content + +
+ ); + cy.screenshot(); + }); +}); + +describe("Tab visual", () => { + it("unselected tab", () => { + cy.mount( + + Content + Content + + ); + cy.screenshot(); + }); + + it("selected tab", () => { + cy.mount( + + Selected content + Other content + + ); + cy.screenshot(); + }); + + it("disabled tab", () => { + cy.mount( + + Active content + Disabled content + + ); + cy.screenshot(); + }); + + it("tab with icon and text", () => { + cy.mount( + + Content + Content + + ); + cy.screenshot(); + }); + + it("tab design — Positive", () => { + cy.mount( + + Content + Content + + ); + cy.screenshot(); + }); + + it("tab design — Negative", () => { + cy.mount( + + Content + Content + + ); + cy.screenshot(); + }); + + it("tab design — Critical", () => { + cy.mount( + + Content + Content + + ); + cy.screenshot(); + }); + + it("tab with additional text", () => { + cy.mount( + + Content + Content + + ); + cy.screenshot(); + }); +}); + +describe("TabSeparator visual", () => { + it("single separator between tabs", () => { + cy.mount( + + One content + Two content + + Three content + Four content + + ); + cy.screenshot(); + }); + + it("multiple separators", () => { + cy.mount( + + One content + + Two content + Three content + + Four content + + ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Text.cy.tsx b/packages/main/cypress/specs/visuals/Text.cy.tsx new file mode 100644 index 000000000000..8d4bc9608c31 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Text.cy.tsx @@ -0,0 +1,94 @@ +import Text from "../../../src/Text.js"; + +const LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"; + +describe("Text visual", () => { + it("basic state", () => { + cy.mount(Simple text); + cy.screenshot(); + }); + + it("long wrapping text — full width", () => { + cy.mount({LOREM}); + cy.screenshot(); + }); + + it("long wrapping text — narrow (6rem)", () => { + cy.mount( +
+ {LOREM} +
+ ); + cy.screenshot(); + }); + + it("maxLines = 1 — full width", () => { + cy.mount({LOREM}); + cy.screenshot(); + }); + + it("maxLines = 1 — narrow (6rem)", () => { + cy.mount( +
+ {LOREM} +
+ ); + cy.screenshot(); + }); + + it("maxLines = 2 — full width", () => { + cy.mount({LOREM}); + cy.screenshot(); + }); + + it("maxLines = 2 — narrow (6rem)", () => { + cy.mount( +
+ {LOREM} +
+ ); + cy.screenshot(); + }); + + it("maxLines = 3 — full width", () => { + cy.mount({LOREM}); + cy.screenshot(); + }); + + it("maxLines = 3 — narrow (6rem)", () => { + cy.mount( +
+ {LOREM} +
+ ); + cy.screenshot(); + }); + + it("maxLines = 4 — full width", () => { + cy.mount({LOREM}); + cy.screenshot(); + }); + + it("maxLines = 4 — narrow (6rem)", () => { + cy.mount( +
+ {LOREM} +
+ ); + cy.screenshot(); + }); + + it("empty indicator mode On", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ {LOREM} +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/TextArea.cy.tsx b/packages/main/cypress/specs/visuals/TextArea.cy.tsx new file mode 100644 index 000000000000..818bd901a2cf --- /dev/null +++ b/packages/main/cypress/specs/visuals/TextArea.cy.tsx @@ -0,0 +1,134 @@ +import TextArea from "../../../src/TextArea.js"; + +describe("TextArea visual", () => { + it("basic state", () => { + cy.mount( + ); + cy.screenshot(); + }); + + it("value state — Negative — focused", () => { + cy.mount( + + ); + cy.get("[ui5-textarea]").shadow().find("textarea").realClick(); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount( + + ); + cy.get("[ui5-textarea]").shadow().find("textarea").realClick(); + cy.screenshot(); + }); + + it("value state — Positive", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount( + + ); + cy.get("[ui5-textarea]").shadow().find("textarea").realClick(); + cy.screenshot(); + }); + + it("value state — Information", () => { + cy.mount( + + ); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount( + + ); + cy.get("[ui5-textarea]").shadow().find("textarea").realClick(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/TimePicker.cy.tsx b/packages/main/cypress/specs/visuals/TimePicker.cy.tsx new file mode 100644 index 000000000000..ef2fa5e4b7aa --- /dev/null +++ b/packages/main/cypress/specs/visuals/TimePicker.cy.tsx @@ -0,0 +1,127 @@ +import TimePicker from "../../../src/TimePicker.js"; + +describe("TimePicker visual", () => { + beforeEach(() => { + cy.clock(new Date("Jan 15, 2024").getTime(), ["Date"]); + }); + + it("basic state", () => { + cy.mount(); + cy.screenshot(); + }); + + it("with value", () => { + cy.mount(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ +
+ ); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount(); + cy.screenshot(); + }); + + it("readonly", () => { + cy.mount(); + cy.screenshot(); + }); + + it("picker open — clocks visible", () => { + cy.mount(); + cy.get("[ui5-time-picker]").ui5TimePickerValueHelpIconPress(); + cy.get("[ui5-time-picker]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Negative", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Negative — picker open", () => { + cy.mount(); + cy.get("[ui5-time-picker]").ui5TimePickerValueHelpIconPress(); + cy.get("[ui5-time-picker]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Negative — focused", () => { + cy.mount(); + cy.get("[ui5-time-picker]") + .shadow().find("[ui5-datetime-input]") + .shadow().find("input") + .realClick(); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Critical — picker open", () => { + cy.mount(); + cy.get("[ui5-time-picker]").ui5TimePickerValueHelpIconPress(); + cy.get("[ui5-time-picker]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Critical — focused", () => { + cy.mount(); + cy.get("[ui5-time-picker]") + .shadow().find("[ui5-datetime-input]") + .shadow().find("input") + .realClick(); + cy.screenshot(); + }); + + it("value state — Positive", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Positive — picker open", () => { + cy.mount(); + cy.get("[ui5-time-picker]").ui5TimePickerValueHelpIconPress(); + cy.get("[ui5-time-picker]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Positive — focused", () => { + cy.mount(); + cy.get("[ui5-time-picker]") + .shadow().find("[ui5-datetime-input]") + .shadow().find("input") + .realClick(); + cy.screenshot(); + }); + + it("value state — Information", () => { + cy.mount(); + cy.screenshot(); + }); + + it("value state — Information — picker open", () => { + cy.mount(); + cy.get("[ui5-time-picker]").ui5TimePickerValueHelpIconPress(); + cy.get("[ui5-time-picker]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("value state — Information — focused", () => { + cy.mount(); + cy.get("[ui5-time-picker]") + .shadow().find("[ui5-datetime-input]") + .shadow().find("input") + .realClick(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Title.cy.tsx b/packages/main/cypress/specs/visuals/Title.cy.tsx new file mode 100644 index 000000000000..a5ef8a308e97 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Title.cy.tsx @@ -0,0 +1,68 @@ +import Title from "../../../src/Title.js"; + +describe("Title visual", () => { + it("all heading levels", () => { + cy.mount( +
+ Title H1 + Title H2 + Title H3 + Title H4 + Title H5 + Title H6 +
+ ); + cy.screenshot(); + }); + + it("all size variants", () => { + cy.mount( +
+ Title size H1 + Title size H2 + Title size H3 + Title size H4 + Title size H5 + Title size H6 +
+ ); + cy.screenshot(); + }); + + it("default level and size", () => { + cy.mount(Default Title); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ Title H1 + Title H2 + Title H3 + Title H4 + Title H5 + Title H6 +
+ ); + cy.screenshot(); + }); + + it("wrappingType None — truncated", () => { + cy.mount( +
+ This long title should be truncated and not wrap to the next line +
+ ); + cy.screenshot(); + }); + + it("wrappingType Normal — wraps", () => { + cy.mount( +
+ This long title should wrap to multiple lines in the available space +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Toast.cy.tsx b/packages/main/cypress/specs/visuals/Toast.cy.tsx new file mode 100644 index 000000000000..99cdeb48fd24 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Toast.cy.tsx @@ -0,0 +1,77 @@ +import Toast from "../../../src/Toast.js"; + +describe("Toast visual", () => { + it("basic — BottomCenter (default placement)", () => { + cy.mount(Toast message); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("placement — TopStart", () => { + cy.mount(TopStart Toast); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("placement — TopCenter", () => { + cy.mount(TopCenter Toast); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("placement — TopEnd", () => { + cy.mount(TopEnd Toast); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("placement — MiddleStart", () => { + cy.mount(MiddleStart Toast); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("placement — MiddleCenter", () => { + cy.mount(MiddleCenter Toast); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("placement — MiddleEnd", () => { + cy.mount(MiddleEnd Toast); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("placement — BottomStart", () => { + cy.mount(BottomStart Toast); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("placement — BottomEnd", () => { + cy.mount(BottomEnd Toast); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("long text content", () => { + cy.mount( + + This is a longer toast message that may wrap onto multiple lines depending on the container width. + + ); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); + + it("custom width and height", () => { + cy.mount( + + Styled Toast + + ); + cy.get("[ui5-toast]").should("be.visible"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ToggleButton.cy.tsx b/packages/main/cypress/specs/visuals/ToggleButton.cy.tsx new file mode 100644 index 000000000000..65eb6d67c6f2 --- /dev/null +++ b/packages/main/cypress/specs/visuals/ToggleButton.cy.tsx @@ -0,0 +1,124 @@ +import ToggleButton from "../../../src/ToggleButton.js"; +import favorite from "@ui5/webcomponents-icons/dist/favorite.js"; + +describe("ToggleButton visual", () => { + it("basic state", () => { + cy.mount(Toggle Button); + cy.screenshot(); + }); + + it("pressed state", () => { + cy.mount(Toggle Button); + cy.screenshot(); + }); + + it("disabled state", () => { + cy.mount(Toggle Button); + cy.screenshot(); + }); + + it("disabled pressed state", () => { + cy.mount(Toggle Button); + cy.screenshot(); + }); + + it("with icon — unpressed", () => { + cy.mount(Favorite); + cy.screenshot(); + }); + + it("with icon — pressed", () => { + cy.mount(Favorite); + cy.screenshot(); + }); + + it("icon only — unpressed", () => { + cy.mount(); + cy.screenshot(); + }); + + it("icon only — pressed", () => { + cy.mount(); + cy.screenshot(); + }); + + it("design Emphasized — unpressed", () => { + cy.mount(Emphasized); + cy.screenshot(); + }); + + it("design Emphasized — pressed", () => { + cy.mount(Emphasized); + cy.screenshot(); + }); + + it("design Positive — unpressed", () => { + cy.mount(Positive); + cy.screenshot(); + }); + + it("design Positive — pressed", () => { + cy.mount(Positive); + cy.screenshot(); + }); + + it("design Negative — unpressed", () => { + cy.mount(Negative); + cy.screenshot(); + }); + + it("design Negative — pressed", () => { + cy.mount(Negative); + cy.screenshot(); + }); + + it("design Transparent — unpressed", () => { + cy.mount(Transparent); + cy.screenshot(); + }); + + it("design Transparent — pressed", () => { + cy.mount(Transparent); + cy.screenshot(); + }); + + it("design Attention — unpressed", () => { + cy.mount(Attention); + cy.screenshot(); + }); + + it("design Attention — pressed", () => { + cy.mount(Attention); + cy.screenshot(); + }); + + it("compact mode — basic", () => { + cy.mount( +
+ Toggle Button +
+ ); + cy.screenshot(); + }); + + it("compact mode — pressed", () => { + cy.mount( +
+ Toggle Button +
+ ); + cy.screenshot(); + }); + + it("focused — unpressed", () => { + cy.mount(Toggle Button); + cy.get("[ui5-toggle-button]").shadow().find(".ui5-button-root").focus(); + cy.screenshot(); + }); + + it("focused — pressed", () => { + cy.mount(Toggle Button); + cy.get("[ui5-toggle-button]").shadow().find(".ui5-button-root").focus(); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Token.cy.tsx b/packages/main/cypress/specs/visuals/Token.cy.tsx new file mode 100644 index 000000000000..de624cc02d7e --- /dev/null +++ b/packages/main/cypress/specs/visuals/Token.cy.tsx @@ -0,0 +1,62 @@ +import Tokenizer from "../../../src/Tokenizer.js"; +import Token from "../../../src/Token.js"; + +describe("Token visual", () => { + it("basic state", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("selected", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("readonly", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("focused", () => { + cy.mount( + + + + ); + cy.get("[ui5-token]").realClick(); + cy.screenshot(); + }); + + it("long text", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Tokenizer.cy.tsx b/packages/main/cypress/specs/visuals/Tokenizer.cy.tsx new file mode 100644 index 000000000000..9c94baefcb9b --- /dev/null +++ b/packages/main/cypress/specs/visuals/Tokenizer.cy.tsx @@ -0,0 +1,100 @@ +import Tokenizer from "../../../src/Tokenizer.js"; +import Token from "../../../src/Token.js"; + +describe("Tokenizer visual", () => { + it("basic state", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("with selected tokens", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("readonly", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("overflow — nMore label visible", () => { + cy.mount( +
+ + + + + + + +
+ ); + cy.get("[ui5-tokenizer]").shadow().find(".ui5-tokenizer-more-text").should("exist"); + cy.screenshot(); + }); + + it("expanded — all tokens visible", () => { + cy.mount( +
+ + + + + + + +
+ ); + cy.get("[ui5-tokenizer]").shadow().find(".ui5-tokenizer-more-text").should("exist").realClick(); + cy.get("[ui5-tokenizer]").should("have.attr", "expanded"); + cy.screenshot(); + }); + + it("nMore popover open", () => { + cy.mount( +
+ + + + + + + +
+ ); + cy.get("[ui5-tokenizer]").shadow().find(".ui5-tokenizer-more-text").should("exist").realClick(); + cy.get("[ui5-tokenizer]").shadow().find("[ui5-responsive-popover]").ui5ResponsivePopoverOpened(); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Toolbar.cy.tsx b/packages/main/cypress/specs/visuals/Toolbar.cy.tsx new file mode 100644 index 000000000000..422a406d7d97 --- /dev/null +++ b/packages/main/cypress/specs/visuals/Toolbar.cy.tsx @@ -0,0 +1,67 @@ +import Toolbar from "../../../src/Toolbar.js"; +import ToolbarButton from "../../../src/ToolbarButton.js"; +import ToolbarSelect from "../../../src/ToolbarSelect.js"; +import ToolbarSelectOption from "../../../src/ToolbarSelectOption.js"; +import ToolbarSeparator from "../../../src/ToolbarSeparator.js"; +import ToolbarSpacer from "../../../src/ToolbarSpacer.js"; +import add from "@ui5/webcomponents-icons/dist/add.js"; +import employee from "@ui5/webcomponents-icons/dist/employee.js"; +import decline from "@ui5/webcomponents-icons/dist/decline.js"; + +describe("Toolbar visual", () => { + it("basic state — buttons, separator, select", () => { + cy.mount( + + + + + + Option 1 + Option 2 + Option 3 + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + + +
+ ); + cy.screenshot(); + }); + + it("with spacer — right-aligned item", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("overflow popover open — AlwaysOverflow items", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-toolbar]").shadow().find(".ui5-tb-overflow-btn").realClick(); + cy.get("[ui5-toolbar]").shadow().find(".ui5-overflow-popover").should("have.attr", "open", "open"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ToolbarButton.cy.tsx b/packages/main/cypress/specs/visuals/ToolbarButton.cy.tsx new file mode 100644 index 000000000000..a07b6242013c --- /dev/null +++ b/packages/main/cypress/specs/visuals/ToolbarButton.cy.tsx @@ -0,0 +1,80 @@ +import Toolbar from "../../../src/Toolbar.js"; +import ToolbarButton from "../../../src/ToolbarButton.js"; +import add from "@ui5/webcomponents-icons/dist/add.js"; +import employee from "@ui5/webcomponents-icons/dist/employee.js"; + +describe("ToolbarButton visual", () => { + it("basic state — text only", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("with icon", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("icon only", () => { + cy.mount( + + + + + ); + cy.screenshot(); + }); + + it("design variants", () => { + cy.mount( + + + + + + + + ); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + +
+ ); + cy.screenshot(); + }); + + it("in overflow popover", () => { + cy.mount( + + + + + ); + cy.get("[ui5-toolbar]").shadow().find(".ui5-tb-overflow-btn").realClick(); + cy.get("[ui5-toolbar]").shadow().find(".ui5-overflow-popover").should("have.attr", "open", "open"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ToolbarItem.cy.tsx b/packages/main/cypress/specs/visuals/ToolbarItem.cy.tsx new file mode 100644 index 000000000000..3b330f8d3d2f --- /dev/null +++ b/packages/main/cypress/specs/visuals/ToolbarItem.cy.tsx @@ -0,0 +1,57 @@ +import Toolbar from "../../../src/Toolbar.js"; +import ToolbarItem from "../../../src/ToolbarItem.js"; +import Button from "../../../src/Button.js"; +import Switch from "../../../src/Switch.js"; + +describe("ToolbarItem visual", () => { + it("basic state — wrapping a button", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("wrapping a switch", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + +
+ ); + cy.screenshot(); + }); + + it("in overflow popover", () => { + cy.mount( + + + + + + + + + ); + cy.get("[ui5-toolbar]").shadow().find(".ui5-tb-overflow-btn").realClick(); + cy.get("[ui5-toolbar]").shadow().find(".ui5-overflow-popover").should("have.attr", "open", "open"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ToolbarSelect.cy.tsx b/packages/main/cypress/specs/visuals/ToolbarSelect.cy.tsx new file mode 100644 index 000000000000..d746ff0f0f3f --- /dev/null +++ b/packages/main/cypress/specs/visuals/ToolbarSelect.cy.tsx @@ -0,0 +1,88 @@ +import Toolbar from "../../../src/Toolbar.js"; +import ToolbarSelect from "../../../src/ToolbarSelect.js"; +import ToolbarSelectOption from "../../../src/ToolbarSelectOption.js"; + +describe("ToolbarSelect visual", () => { + it("basic state", () => { + cy.mount( + + + Option 1 + Option 2 + Option 3 + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + Option 1 + Option 2 + Option 3 + + +
+ ); + cy.screenshot(); + }); + + it("disabled", () => { + cy.mount( + + + Option 1 + Option 2 + Option 3 + + + ); + cy.screenshot(); + }); + + it("value state — Critical", () => { + cy.mount( + + + Option 1 + Option 2 + Option 3 + + + ); + cy.screenshot(); + }); + + it("dropdown open", () => { + cy.mount( + + + Option 1 + Option 2 + Option 3 + + + ); + cy.get("[ui5-toolbar-select]").shadow().find("[ui5-select]").realClick(); + cy.screenshot(); + }); + + it("in overflow popover", () => { + cy.mount( + + + Option 1 + Option 2 + Option 3 + + + ); + cy.get("[ui5-toolbar]").shadow().find(".ui5-tb-overflow-btn").realClick(); + cy.get("[ui5-toolbar]").shadow().find(".ui5-overflow-popover").should("have.attr", "open", "open"); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ToolbarSeparator.cy.tsx b/packages/main/cypress/specs/visuals/ToolbarSeparator.cy.tsx new file mode 100644 index 000000000000..7a73ee7fcae7 --- /dev/null +++ b/packages/main/cypress/specs/visuals/ToolbarSeparator.cy.tsx @@ -0,0 +1,44 @@ +import Toolbar from "../../../src/Toolbar.js"; +import ToolbarButton from "../../../src/ToolbarButton.js"; +import ToolbarSeparator from "../../../src/ToolbarSeparator.js"; +import add from "@ui5/webcomponents-icons/dist/add.js"; +import decline from "@ui5/webcomponents-icons/dist/decline.js"; + +describe("ToolbarSeparator visual", () => { + it("basic state — separator between buttons", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + +
+ ); + cy.screenshot(); + }); + + it("multiple separators", () => { + cy.mount( + + + + + + + + ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/ToolbarSpacer.cy.tsx b/packages/main/cypress/specs/visuals/ToolbarSpacer.cy.tsx new file mode 100644 index 000000000000..af2a43ba5f9e --- /dev/null +++ b/packages/main/cypress/specs/visuals/ToolbarSpacer.cy.tsx @@ -0,0 +1,42 @@ +import Toolbar from "../../../src/Toolbar.js"; +import ToolbarButton from "../../../src/ToolbarButton.js"; +import ToolbarSpacer from "../../../src/ToolbarSpacer.js"; +import add from "@ui5/webcomponents-icons/dist/add.js"; +import decline from "@ui5/webcomponents-icons/dist/decline.js"; + +describe("ToolbarSpacer visual", () => { + it("flexible spacer — pushes item to the right", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("fixed-width spacer", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/Tree.cy.tsx b/packages/main/cypress/specs/visuals/Tree.cy.tsx new file mode 100644 index 000000000000..a4647c7ec27c --- /dev/null +++ b/packages/main/cypress/specs/visuals/Tree.cy.tsx @@ -0,0 +1,134 @@ +import Tree from "../../../src/Tree.js"; +import TreeItem from "../../../src/TreeItem.js"; +import paste from "@ui5/webcomponents-icons/dist/paste.js"; +import copy from "@ui5/webcomponents-icons/dist/copy.js"; + +describe("Tree visual", () => { + it("basic state — flat items", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("nested items — collapsed", () => { + cy.mount( + + + + + + + + + + + + + ); + cy.screenshot(); + }); + + it("nested items — expanded", () => { + cy.mount( + + + + + + + + + + + + + ); + cy.screenshot(); + }); + + it("with additionalText and additionalTextState", () => { + cy.mount( + + + + + + + + + + ); + cy.screenshot(); + }); + + it("selectionMode Single", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("selectionMode Multiple", () => { + cy.mount( + + + + + + + + + ); + cy.screenshot(); + }); + + it("selectionMode Delete", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("noDataText", () => { + cy.mount(); + cy.screenshot(); + }); + + it("headerText and footerText", () => { + cy.mount( + + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/TreeItem.cy.tsx b/packages/main/cypress/specs/visuals/TreeItem.cy.tsx new file mode 100644 index 000000000000..1b5674dff30d --- /dev/null +++ b/packages/main/cypress/specs/visuals/TreeItem.cy.tsx @@ -0,0 +1,129 @@ +import Tree from "../../../src/Tree.js"; +import TreeItem from "../../../src/TreeItem.js"; +import Icon from "../../../src/Icon.js"; +import paste from "@ui5/webcomponents-icons/dist/paste.js"; +import copy from "@ui5/webcomponents-icons/dist/copy.js"; +import bell from "@ui5/webcomponents-icons/dist/bell.js"; + +describe("TreeItem visual", () => { + it("basic state", () => { + cy.mount( + + + + ); + cy.screenshot(); + }); + + it("with icon", () => { + cy.mount( + + + + + + ); + cy.screenshot(); + }); + + it("with image slot", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("with additionalText — all states", () => { + cy.mount( + + + + + + + + ); + cy.screenshot(); + }); + + it("expanded with children", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("collapsed with children", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("selected", () => { + cy.mount( + + + + + ); + cy.screenshot(); + }); + + it("indeterminate — Multiple mode", () => { + cy.mount( + + + + + + + ); + cy.screenshot(); + }); + + it("deeply nested — multiple levels", () => { + cy.mount( + + + + + + + + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + + + +
+ ); + cy.screenshot(); + }); +}); diff --git a/packages/main/cypress/specs/visuals/TreeItemCustom.cy.tsx b/packages/main/cypress/specs/visuals/TreeItemCustom.cy.tsx new file mode 100644 index 000000000000..88512f3ae82c --- /dev/null +++ b/packages/main/cypress/specs/visuals/TreeItemCustom.cy.tsx @@ -0,0 +1,108 @@ +import Tree from "../../../src/Tree.js"; +import TreeItemCustom from "../../../src/TreeItemCustom.js"; +import Button from "../../../src/Button.js"; + +describe("TreeItemCustom visual", () => { + it("basic state — custom content", () => { + cy.mount( + + + Custom content item 1 + + + Custom content item 2 + + + ); + cy.screenshot(); + }); + + it("with button content", () => { + cy.mount( + + + + + + + + + ); + cy.screenshot(); + }); + + it("expanded with nested custom items", () => { + cy.mount( + + + Level 1 + + Level 2 + + Level 3 + + + + + ); + cy.screenshot(); + }); + + it("collapsed with children", () => { + cy.mount( + + + Parent item + + Child item + + + + ); + cy.screenshot(); + }); + + it("selectionMode Multiple — with hideSelectionElement", () => { + cy.mount( + + + With checkbox + + + Without checkbox + + + ); + cy.screenshot(); + }); + + it("selectionMode Multiple — selected", () => { + cy.mount( + + + Selected item + + + Unselected item + + + ); + cy.screenshot(); + }); + + it("compact mode", () => { + cy.mount( +
+ + + + + + + + +
+ ); + cy.screenshot(); + }); +}); From d8548bc85d59bea44ea098ca02b0bc2a970043b2 Mon Sep 17 00:00:00 2001 From: ilhan orhan Date: Wed, 8 Jul 2026 16:08:41 +0300 Subject: [PATCH 7/7] fix(framework): await pending language change in connectedCallback instead of skipping first render (#13798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(framework): await pending language change instead of skipping first render PR #13602 made connectedCallback bail out early for language-aware components mounted while a language change was in progress. That path silently skipped onEnterDOM, _fullyConnected, and _domRefReadyPromise resolution — so consumers that register listeners or observers in onEnterDOM (e.g. the Udex Footer's ResizeHandler.register call) never saw those side effects wired up. Replace the boolean pending flag with a promise handle so connectedCallback can await it directly and then continue on the normal render path. This keeps the lifecycle invariant intact (onEnterDOM fires exactly once, after the first render) and removes the need for any deferred-lifecycle bookkeeping. Language.ts: - languageChangePending changes from boolean to Promise | null. - A single startLanguageChange helper builds the promise, runs the deferred reRenderAllUI5Elements, and clears the field when done (only if a newer change hasn't replaced it). - setLanguage() and the cross-runtime attachConfigChange handler share the helper, so both paths keep the field consistent. - getLanguageChangePending() now returns the in-flight promise or null. UI5Element.ts: - connectedCallback awaits the pending promise for language-aware components, then proceeds normally. If the deferred re-render already rendered the element during the await, skip the redundant renderImmediately. - _invalidate keeps its existing guard - a non-null promise is truthy, so the boolean check still works semantically. Tests: - New LanguageAwareLifecycle test element (languageAware: true, counts hook calls). - New base/cypress/specs/LanguageAwareLifecycle.cy.tsx covering: * onEnterDOM fires exactly once after a pending language change resolves * onEnterDOM does not fire a second time on a subsequent re-render * onEnterDOM does not fire if the element is removed before rendering Verified locally: - 3/3 pass with the fix, 2/3 fail without it. - Existing DatePicker_cldr_race and i18n_texts specs still pass. --- .../specs/LanguageAwareLifecycle.cy.tsx | 104 ++++++++++++++++++ packages/base/src/UI5Element.ts | 17 ++- packages/base/src/config/Language.ts | 47 ++++---- .../test-elements/LanguageAwareLifecycle.tsx | 40 +++++++ 4 files changed, 179 insertions(+), 29 deletions(-) create mode 100644 packages/base/cypress/specs/LanguageAwareLifecycle.cy.tsx create mode 100644 packages/base/test/test-elements/LanguageAwareLifecycle.tsx diff --git a/packages/base/cypress/specs/LanguageAwareLifecycle.cy.tsx b/packages/base/cypress/specs/LanguageAwareLifecycle.cy.tsx new file mode 100644 index 000000000000..48aaebb4b01b --- /dev/null +++ b/packages/base/cypress/specs/LanguageAwareLifecycle.cy.tsx @@ -0,0 +1,104 @@ +import { setLanguage } from "../../src/config/Language.js"; +import { registerI18nLoader } from "../../src/i18nBundle.js"; +import parseProperties from "../../src/PropertiesFileFormat.js"; +import LanguageAwareLifecycle from "../../test/test-elements/LanguageAwareLifecycle.js"; + +// Regression test for the language-aware lifecycle bug introduced by PR #13602. +// +// Before the fix, if setLanguage() was called without awaiting and a language-aware +// component was inserted into the DOM while languageChangePending was true, +// connectedCallback bailed out with a bare `return`. The deferred reRenderAllUI5Elements +// call re-rendered the shadow DOM but the onEnterDOM lifecycle hook was never fired, +// so consumers that register a ResizeHandler / add DOM listeners / set attributes +// inside onEnterDOM saw those side effects silently drop. +describe("Language-aware component lifecycle when setLanguage is not awaited", () => { + beforeEach(() => { + // Register a slow custom-language loader so setLanguage("bg") stays pending + // long enough for the component to be inserted before CLDR resolves. + cy.wrap({ registerI18nLoader }) + .then(api => { + api.registerI18nLoader("lifecycle-lang-test", "bg", () => { + return new Promise(resolve => { + setTimeout(() => resolve(parseProperties("KEY=Value")), 50); + }); + }); + }); + + // Reset language back to en between tests + cy.wrap({ setLanguage }) + .then(async api => { + await api.setLanguage("en"); + }); + }); + + it("fires onEnterDOM exactly once after the deferred first render", () => { + // Kick off a language change but do NOT await the promise - this is the + // pattern the Udex Footer's host app was using. + let languageReady: Promise; + cy.wrap({ setLanguage }) + .then(api => { + languageReady = api.setLanguage("bg"); + }); + + cy.mount(); + + // Wait for the language change to settle + cy.then(() => languageReady); + + // After the deferred re-render, onEnterDOM must have fired exactly once + // and the component must be marked as fully connected. + cy.get("[ui5-language-aware-lifecycle]") + .should($el => { + const el = $el[0]; + expect(el.enterDOMCount, "onEnterDOM call count").to.equal(1); + expect(el._fullyConnected, "_fullyConnected flag").to.equal(true); + expect(el.afterRenderingCount, "onAfterRendering call count").to.be.at.least(1); + }); + }); + + it("does not fire onEnterDOM twice on subsequent language re-renders", () => { + let languageReady: Promise; + cy.wrap({ setLanguage }) + .then(api => { + languageReady = api.setLanguage("bg"); + }); + + cy.mount(); + + cy.then(() => languageReady); + + // Change language a second time - this triggers reRenderAllUI5Elements + // again but the element is already fully connected, so onEnterDOM must + // NOT be called again. + cy.wrap({ setLanguage }) + .then(async api => { + await api.setLanguage("en"); + }); + + cy.get("[ui5-language-aware-lifecycle]") + .should($el => { + expect($el[0].enterDOMCount, "onEnterDOM should still be 1 after re-render").to.equal(1); + }); + }); + + it("does not fire onEnterDOM if element is removed before deferred render", () => { + cy.wrap({ setLanguage }) + .then(async api => { + // Insert then synchronously remove the element while the language + // change is still pending. The element is unregistered by + // disconnectedCallback, so reRenderAllUI5Elements must not touch it + // and its lifecycle hooks must stay silent. + const el = document.createElement("ui5-language-aware-lifecycle") as LanguageAwareLifecycle; + + const languageReady = api.setLanguage("bg"); // NOT awaited yet + document.body.appendChild(el); + document.body.removeChild(el); + + await languageReady; + + expect(el.enterDOMCount, "onEnterDOM should not fire on removed element").to.equal(0); + expect(el.exitDOMCount, "onExitDOM should not fire (was never fully connected)").to.equal(0); + expect(el._fullyConnected, "_fullyConnected flag stays false").to.equal(false); + }); + }); +}); diff --git a/packages/base/src/UI5Element.ts b/packages/base/src/UI5Element.ts index e298481d72d7..5170eba6530f 100644 --- a/packages/base/src/UI5Element.ts +++ b/packages/base/src/UI5Element.ts @@ -357,17 +357,24 @@ abstract class UI5Element extends HTMLElement { await ctor._definePromise; } - // Skip rendering while a language change is in progress to avoid rendering with not fully loaded locale data. - // Once the locale data is loaded, the language-aware component will be re-rendered. - if (ctor.getMetadata().isLanguageAware() && getLanguageChangePending()) { - return; + // Wait for any pending language change to finish before rendering to avoid rendering + // with not fully loaded locale data. Once it resolves, proceed with the normal render + // path so onEnterDOM and the rest of the lifecycle fire exactly as they would otherwise. + // Note: the reRenderAllUI5Elements call that closes out the language change may already + // have rendered this element via the deferred queue (since it was registered above), so + // we skip renderImmediately if the first render has already happened. + const languageChangePending = getLanguageChangePending(); + if (ctor.getMetadata().isLanguageAware() && languageChangePending) { + await languageChangePending; } if (!this._inDOM) { // Component removed from DOM while _processChildren was running return; } - renderImmediately(this); + if (!this._rendered) { + renderImmediately(this); + } this._domRefReadyPromise._deferredResolve!(); this._fullyConnected = true; this.onEnterDOM(); diff --git a/packages/base/src/config/Language.ts b/packages/base/src/config/Language.ts index b6fdf96e55e1..1ab2896adbe8 100644 --- a/packages/base/src/config/Language.ts +++ b/packages/base/src/config/Language.ts @@ -17,27 +17,33 @@ attachConfigurationReset(() => { fetchDefaultLanguage = undefined; }); -// Flag indicating that a language change is in progress and not yet complete. -// While this flag is true, language-aware components will not re-render. -// These components may rely on language-specific data (e.g., CLDR, language bundles), -// which might be unavailable during the loading phase. -// During this phase, all re-rendering is postponed. -// Once all necessary language data has been loaded, the language change -// will trigger a re-render of all language-aware components. -let languageChangePending = false; - -attachConfigChange("language", (language: string) => { - curLanguage = language; - languageChangePending = true; - fireLanguageChange(language).then(() => { - languageChangePending = false; +// Promise that resolves when the current language change (i18n bundles + CLDR data) +// completes, or `null` when no language change is in flight. Consumers that need to +// wait for locale data to be ready before rendering — most notably language-aware +// UI5Element instances mounted while setLanguage is in flight — can await it. +let languageChangePending: Promise | null = null; + +const startLanguageChange = (language: string): Promise => { + const changePromise = fireLanguageChange(language).then(() => { if (isBooted()) { - reRenderAllUI5Elements({ languageAware: true }); + return reRenderAllUI5Elements({ languageAware: true }); + } + }).finally(() => { + // Only clear if no newer change has already replaced us + if (languageChangePending === changePromise) { + languageChangePending = null; } }); + languageChangePending = changePromise; + return changePromise; +}; + +attachConfigChange("language", (language: string) => { + curLanguage = language; + startLanguageChange(language); }); -const getLanguageChangePending = () => languageChangePending; +const getLanguageChangePending = (): Promise | null => languageChangePending; /** * Returns the currently configured language, or the browser language as a fallback. @@ -64,18 +70,11 @@ const setLanguage = async (language: string): Promise => { return; } - languageChangePending = true; curLanguage = language; fireConfigChange("language", language); - await fireLanguageChange(language); - - languageChangePending = false; - - if (isBooted()) { - await reRenderAllUI5Elements({ languageAware: true }); - } + await startLanguageChange(language); }; /** diff --git a/packages/base/test/test-elements/LanguageAwareLifecycle.tsx b/packages/base/test/test-elements/LanguageAwareLifecycle.tsx new file mode 100644 index 000000000000..20aabb16e9a7 --- /dev/null +++ b/packages/base/test/test-elements/LanguageAwareLifecycle.tsx @@ -0,0 +1,40 @@ +import UI5Element from "../../src/UI5Element.js"; +import customElement from "../../src/decorators/customElement.js"; +import jsxRenderer from "../../src/renderer/JsxRenderer.js"; + +/** + * Language-aware test element that records how many times each lifecycle hook fires. + * Used by base/cypress/specs/LanguageAwareLifecycle.cy.tsx to verify that onEnterDOM + * still fires exactly once, even when connectedCallback runs while a language change + * is pending (see fix for the PR #13602 regression). + */ +@customElement({ + tag: "ui5-language-aware-lifecycle", + renderer: jsxRenderer, + languageAware: true, +}) +class LanguageAwareLifecycle extends UI5Element { + enterDOMCount = 0; + exitDOMCount = 0; + afterRenderingCount = 0; + + onEnterDOM() { + this.enterDOMCount++; + } + + onExitDOM() { + this.exitDOMCount++; + } + + onAfterRendering() { + this.afterRenderingCount++; + } + + static get template() { + return () =>
lifecycle
; + } +} + +LanguageAwareLifecycle.define(); + +export default LanguageAwareLifecycle;