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;
}
}