diff --git a/packages/main/cypress/specs/Select.cy.tsx b/packages/main/cypress/specs/Select.cy.tsx index 76d26b7aed5c..54ae56d3906f 100644 --- a/packages/main/cypress/specs/Select.cy.tsx +++ b/packages/main/cypress/specs/Select.cy.tsx @@ -1001,22 +1001,27 @@ describe("Select general interaction", () => { const EXPECTED_SELECTION_TEXT2 = "Condensed"; cy.get("@select").realClick(); + cy.get("@select").should("have.attr", "opened"); cy.get("@select").realPress("Escape"); - - cy.get("@select").realPress("ArrowUp"); - + cy.get("@select").should("not.have.attr", "opened"); + cy.get("@select") .shadow() .find(".ui5-select-label-root") + .as("labelRoot") + .focus() + .should("be.focused"); + + cy.get("@labelRoot").realPress("ArrowUp"); + + cy.get("@labelRoot") .should("contain.text", EXPECTED_SELECTION_TEXT1); cy.get("@select").should("have.prop", "value", EXPECTED_SELECTION_TEXT1); - cy.get("@select").realPress("ArrowDown"); + cy.get("@labelRoot").realPress("ArrowDown"); - cy.get("@select") - .shadow() - .find(".ui5-select-label-root") + cy.get("@labelRoot") .should("contain.text", EXPECTED_SELECTION_TEXT2); cy.get("@select").should("have.prop", "value", EXPECTED_SELECTION_TEXT2); @@ -1045,24 +1050,40 @@ describe("Select general interaction", () => { const EXPECTED_SELECTION_TEXT1 = "Compact"; const EXPECTED_SELECTION_TEXT2 = "Condensed"; + // Closed-state arrow navigation with announcement cy.get("@select").realClick(); + cy.get("@select").should("have.attr", "opened"); cy.get("@select").realPress("Escape"); + cy.get("@select").should("not.have.attr", "opened"); + + cy.get("@select") + .shadow() + .find(".ui5-select-label-root") + .as("labelRoot") + .focus() + .should("be.focused"); - cy.get("@select").realPress("ArrowUp"); + cy.get("@labelRoot").realPress("ArrowUp"); cy.get(".ui5-invisiblemessage-polite").should("contain.text", EXPECTED_SELECTION_TEXT1); - cy.get("@select").realPress("ArrowDown"); + cy.get("@labelRoot").realPress("ArrowDown"); cy.get(".ui5-invisiblemessage-polite").should("contain.text", EXPECTED_SELECTION_TEXT2); + // Open picker, navigate, escape (revert) cy.get("@select").realClick(); + cy.get("@select").should("have.attr", "opened"); cy.get("@select").realPress("ArrowUp"); cy.get("@select").realPress("Escape"); + cy.get("@select").should("not.have.attr", "opened"); + // Open picker, navigate, enter (confirm) cy.get("@select").realClick(); + cy.get("@select").should("have.attr", "opened"); cy.get("@select").realPress("ArrowUp"); cy.get("@select").realPress("Enter"); + cy.get("@select").should("not.have.attr", "opened"); cy.get("@select") .shadow() @@ -1140,15 +1161,14 @@ describe("Select general interaction", () => { const EXPECTED_SELECTION_TEXT = "Banana"; - cy.get("[ui5-select]").realClick(); - cy.get("[ui5-select]").realClick(); - - cy.get("[ui5-select]").realPress("Space"); + cy.get("[ui5-select]").as("select").realClick(); + cy.get("@select").should("have.attr", "opened"); - cy.get("[ui5-select]").realPress("ArrowUp"); - cy.get("[ui5-select]").realPress("Tab"); + cy.get("@select").realPress("ArrowUp"); + cy.get("@select").realPress("Tab"); + cy.get("@select").should("not.have.attr", "opened"); - cy.get("[ui5-select]") + cy.get("@select") .shadow() .find(".ui5-select-label-root") .should("contain.text", EXPECTED_SELECTION_TEXT); @@ -1174,20 +1194,22 @@ describe("Select general interaction", () => { const EXPECTED_SELECTION_TEXT = "Watermelon"; - cy.get("[ui5-select]").eq(1).realClick(); - cy.get("[ui5-select]").eq(1).realClick(); - - cy.get("[ui5-select]").eq(1).realPress("Space"); + cy.get("[ui5-select]").eq(1).as("select").realClick(); + cy.get("@select").should("have.attr", "opened"); - cy.get("[ui5-select]").eq(1).realPress("ArrowDown"); - cy.get("[ui5-select]").eq(1).realPress(["Shift", "Tab"]); + cy.get("@select").realPress("ArrowDown"); + cy.get("@select").realPress(["Shift", "Tab"]); + cy.get("@select").should("not.have.attr", "opened"); - cy.get("[ui5-select]").eq(1) + cy.get("@select") .shadow() .find(".ui5-select-label-root") .should("contain.text", EXPECTED_SELECTION_TEXT); - cy.get("[ui5-select]").eq(0).should("be.focused"); + cy.get("[ui5-select]").eq(0) + .shadow() + .find(".ui5-select-label-root") + .should("be.focused"); }); it("tests selection does not cycle with ArrowDown", () => { @@ -1754,24 +1776,32 @@ describe("Select general interaction", () => { ); - cy.get("[ui5-select]").realClick(); - cy.get("[ui5-select]").realPress("s"); + cy.get("[ui5-select]").as("select").realClick(); + cy.get("@select").should("have.attr", "opened"); + cy.get("@select").realPress("s"); - cy.get("[ui5-select]") + cy.get("@select") .shadow() .find(".ui5-select-label-root") .should("contain.text", "Second"); - cy.get("[ui5-select]").realPress("Enter"); - - cy.get("[ui5-select]").realPress("t"); + cy.get("@select").realPress("Enter"); + cy.get("@select").should("not.have.attr", "opened"); - cy.get("[ui5-select]") + // After picker closes, focus the label root before type-to-select + cy.get("@select") .shadow() .find(".ui5-select-label-root") + .as("labelRoot") + .focus() + .should("be.focused"); + + cy.get("@labelRoot").realPress("t"); + + cy.get("@labelRoot") .should("contain.text", "Third"); - cy.get("[ui5-select]").should("have.prop", "value", "Third"); + cy.get("@select").should("have.prop", "value", "Third"); }); it("navigates with ArrowDown when initial value does not match any option", () => { @@ -1819,4 +1849,67 @@ describe("Select general interaction", () => { .should("have.attr", "selected"); cy.get("[ui5-select]").should("have.prop", "value", "C"); }); + + it("fires change event only once when pressing Enter on opened picker", () => { + cy.mount( + + ); + + cy.get("[ui5-select]") + .as("select") + .then(($select) => { + $select[0].addEventListener("ui5-change", cy.stub().as("changeStub")); + }); + + // Open the picker, navigate to a different option, and press Enter + cy.get("@select").realClick(); + cy.get("@select").should("have.attr", "opened"); + + cy.get("@select").realPress("ArrowUp"); + cy.get("@select").realPress("Enter"); + + // The picker should close + cy.get("@select").should("not.have.attr", "opened"); + + // The change event must fire exactly once (not twice due to both + // the list item click and the Select's own Enter key handler) + cy.get("@changeStub").should("have.been.calledOnce"); + + cy.get("@select") + .shadow() + .find(".ui5-select-label-root") + .should("contain.text", "Compact"); + }); + + it("focuses the select root after the picker closes so screen readers can announce the selected value", () => { + cy.mount( + + ); + + cy.get("[ui5-select]") + .as("select"); + + // Open the picker and select a different option + cy.get("@select").realClick(); + cy.get("@select").should("have.attr", "opened"); + + cy.get("@select").realPress("ArrowUp"); + cy.get("@select").realPress("Enter"); + + // The picker should close + cy.get("@select").should("not.have.attr", "opened"); + + // After the picker closes, the select root should be focused + // so that screen readers like NVDA can announce the selected value + cy.get("@select") + .should("be.focused"); + }); }); diff --git a/packages/main/src/Select.ts b/packages/main/src/Select.ts index 25fa0295329a..7b66078d5336 100644 --- a/packages/main/src/Select.ts +++ b/packages/main/src/Select.ts @@ -690,7 +690,7 @@ class Select extends UI5Element implements IFormInputElement { this._handleHomeKey(e); } else if (isEnd(e)) { this._handleEndKey(e); - } else if (isEnter(e)) { + } else if (isEnter(e) && !e.defaultPrevented) { this._handleSelectionChange(); } else if (isUp(e) || isDown(e)) { this._handleArrowNavigation(e); @@ -941,7 +941,7 @@ class Select extends UI5Element implements IFormInputElement { _applyFocusToSelectedItem() { this.options.forEach(option => { option.focused = option.selected; - if (option.focused && isPhone()) { + if (option.focused) { // on phone, the popover opens full screen (dialog) // move focus to option to read out dialog header option.focus(); diff --git a/packages/main/src/SelectPopoverTemplate.tsx b/packages/main/src/SelectPopoverTemplate.tsx index 6a7626a2ef69..f401a69244eb 100644 --- a/packages/main/src/SelectPopoverTemplate.tsx +++ b/packages/main/src/SelectPopoverTemplate.tsx @@ -27,6 +27,7 @@ export default function SelectPopoverTemplate(this: Select) { onBeforeOpen={this._beforeOpen} onClose={this._afterClose} onKeyDown={this._onkeydown} + onKeyPress={this._handleKeyboardNavigation} accessibleName={this._isPhone ? this._headerTitleText : undefined} > {this._isPhone &&