diff --git a/packages/main/cypress/specs/Panel.cy.tsx b/packages/main/cypress/specs/Panel.cy.tsx index fedb0b269c42..f98faaa5db9f 100644 --- a/packages/main/cypress/specs/Panel.cy.tsx +++ b/packages/main/cypress/specs/Panel.cy.tsx @@ -323,6 +323,252 @@ describe("Events", () => { }); }); +describe("Keyboard Interactions", () => { + it("Enter key down expands/collapses panel", () => { + cy.mount( + Content + ); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-header") + .as("header"); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-content") + .as("content"); + + // Initial state - expanded + cy.get("@content") + .should("be.visible"); + + // Press Enter - should trigger toggle immediately + cy.get("@header") + .focus() + .realPress("Enter"); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(50); + + // Content should be collapsed after Enter + cy.get("@content") + .should("not.be.visible"); + + cy.get("@toggleEvent") + .should("have.been.calledOnce"); + + // Press Enter again - should toggle back to expanded + cy.get("@header") + .realPress("Enter"); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(50); + + // Content should be visible again + cy.get("@content") + .should("be.visible"); + + cy.get("@toggleEvent") + .should("have.been.calledTwice"); + }); + + it("Space key with Escape cancellation prevents toggle", () => { + cy.mount( + Content + ); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-header") + .as("header"); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-content") + .as("content"); + + // Initial state - expanded + cy.get("@content") + .should("be.visible"); + + // Press and hold Space - this should set pending toggle but not execute yet + cy.get("@header") + .focus() + .realPress(["Space", "Escape"]); + + // Content should still be visible (toggle was canceled by Escape) + cy.get("@content") + .should("be.visible"); + + cy.get("@toggleEvent") + .should("not.have.been.called"); + + // Verify panel is still in expanded state + cy.get("[ui5-panel]") + .should("not.have.attr", "collapsed"); + }); + + it("Space key without Escape executes toggle", () => { + cy.mount( + Content + ); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-header") + .as("header"); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-content") + .as("content"); + + // Initial state - expanded + cy.get("@content") + .should("be.visible"); + + // Press Space - should execute the toggle + cy.get("@header") + .focus() + .realPress("Space"); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(50); + + // Content should now be collapsed + cy.get("@content") + .should("not.be.visible"); + + cy.get("@toggleEvent") + .should("have.been.calledOnce"); + + // Verify panel is in collapsed state + cy.get("[ui5-panel]") + .should("have.attr", "collapsed"); + }); + + it("Space key interrupted by Escape does not toggle", () => { + cy.mount( + Content + ); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-header") + .as("header"); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-content") + .as("content"); + + // Test the Space + Escape cancellation behavior + cy.get("@header") + .focus() + .realPress(["Space", "Escape"]); + + // Should not have toggled because Escape canceled the Space action + cy.get("@content") + .should("be.visible"); + + cy.get("@toggleEvent") + .should("not.have.been.called"); + }); + + it("Fixed panel (should not toggle)", () => { + cy.mount( + Content + ); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-header") + .as("header"); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-content") + .as("content"); + + // Content should be visible + cy.get("@content") + .should("be.visible"); + + // Try Enter - should not toggle fixed panel + cy.get("@header") + .focus() + .realPress("Enter"); + + cy.get("@content") + .should("be.visible"); + + cy.get("@toggleEvent") + .should("not.have.been.called"); + + // Try Space - should not toggle fixed panel + cy.get("@header") + .realPress("Space"); + + cy.get("@content") + .should("be.visible"); + + cy.get("@toggleEvent") + .should("not.have.been.called"); + }); + + it("Custom header (only button should work)", () => { + cy.mount( +
+ Custom Header +
+ Content +
); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-header") + .as("header"); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-header-button") + .as("toggleButton"); + + cy.get("[ui5-panel]") + .shadow() + .find(".ui5-panel-content") + .as("content"); + + // Enter on custom header area should not toggle + cy.get("@header") + .focus() + .realPress("Enter"); + + cy.get("@content") + .should("be.visible"); + + cy.get("@toggleEvent") + .should("not.have.been.called"); + + // Enter on toggle button should work + cy.get("@toggleButton") + .shadow() + .find(".ui5-button-root") + .focus() + .realPress("Enter"); + + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(50); + + cy.get("@content") + .should("not.be.visible"); + + cy.get("@toggleEvent") + .should("have.been.calledOnce"); + }); +}); + describe("Accessibility", () => { it("Aria attributes on default header", () => { cy.mount( diff --git a/packages/main/src/Panel.ts b/packages/main/src/Panel.ts index c5dae53b7e82..33e19b1ee0fa 100644 --- a/packages/main/src/Panel.ts +++ b/packages/main/src/Panel.ts @@ -6,7 +6,7 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import slideDown from "@ui5/webcomponents-base/dist/animations/slideDown.js"; import slideUp from "@ui5/webcomponents-base/dist/animations/slideUp.js"; -import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js"; +import { isSpace, isEnter, isEscape } from "@ui5/webcomponents-base/dist/Keys.js"; import AnimationMode from "@ui5/webcomponents-base/dist/types/AnimationMode.js"; import { getAnimationMode } from "@ui5/webcomponents-base/dist/config/AnimationMode.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; @@ -170,8 +170,8 @@ class Panel extends UI5Element { * @public * @since 1.16.0-rc.1 */ - @property({ type: Boolean }) - stickyHeader = false; + @property({ type: Boolean }) + stickyHeader = false; /** * When set to `true`, the `accessibleName` property will be @@ -195,6 +195,9 @@ class Panel extends UI5Element { @property({ type: Boolean, noAttribute: true }) _animationRunning = false; + @property({ type: Boolean, noAttribute: true }) + _pendingToggle = false; + /** * Defines the component header area. * @@ -248,11 +251,18 @@ class Panel extends UI5Element { } if (isEnter(e)) { - e.preventDefault(); + this._toggleOpen(); } if (isSpace(e)) { e.preventDefault(); + this._pendingToggle = true; + } + + // Cancel toggle if Escape is pressed + if (isEscape(e) && this._pendingToggle) { + e.preventDefault(); + this._pendingToggle = false; } } @@ -262,11 +272,15 @@ class Panel extends UI5Element { } if (isEnter(e)) { - this._toggleOpen(); + e.preventDefault(); } if (isSpace(e)) { - this._toggleOpen(); + // Only toggle if space was pressed and escape wasn't pressed to cancel + if (this._pendingToggle) { + this._toggleOpen(); + } + this._pendingToggle = false; } }