Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions packages/main/cypress/specs/Panel.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,252 @@ describe("Events", () => {
});
});

describe("Keyboard Interactions", () => {
it("Enter key down expands/collapses panel", () => {
cy.mount(<Panel headerText="Panel" onToggle={cy.stub().as("toggleEvent")}>
<Title level={TitleLevel.H4}>Content</Title>
</Panel>);

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(<Panel headerText="Panel" onToggle={cy.stub().as("toggleEvent")}>
<Title level={TitleLevel.H4}>Content</Title>
</Panel>);

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(<Panel headerText="Panel" onToggle={cy.stub().as("toggleEvent")}>
<Title level={TitleLevel.H4}>Content</Title>
</Panel>);

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(<Panel headerText="Panel" onToggle={cy.stub().as("toggleEvent")}>
<Title level={TitleLevel.H4}>Content</Title>
</Panel>);

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(<Panel headerText="Fixed Panel" fixed={true} onToggle={cy.stub().as("toggleEvent")}>
<Title level={TitleLevel.H4}>Content</Title>
</Panel>);

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(<Panel onToggle={cy.stub().as("toggleEvent")}>
<div slot="header">
<Title level={TitleLevel.H2}>Custom Header</Title>
</div>
<Title level={TitleLevel.H3}>Content</Title>
</Panel>);

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(<Panel headerText="Panel" headerLevel={TitleLevel.H3}>
Expand Down
26 changes: 20 additions & 6 deletions packages/main/src/Panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
}
}

Expand All @@ -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;
}
}

Expand Down
Loading