diff --git a/packages/main/cypress/specs/Select.cy.tsx b/packages/main/cypress/specs/Select.cy.tsx index 76d26b7aed5c..88f3b34ce77f 100644 --- a/packages/main/cypress/specs/Select.cy.tsx +++ b/packages/main/cypress/specs/Select.cy.tsx @@ -592,6 +592,102 @@ describe("Select - Accessibility", () => { expect(accessInfo.label).to.equal("Updated Reference"); // Updated aria label from ref }); }); + + it("should have hidden span with selected option text for screen reader announcement", () => { + cy.mount( + + ); + + // Verify hidden span exists with correct attributes + cy.get("#selectWithHiddenText") + .shadow() + .find("[id$='-selectedOptionText']") + .should("exist") + .should("have.class", "ui5-hidden-text") + .should("have.attr", "role", "option") + .should("have.attr", "aria-selected", "true") + .should("have.text", "Option 2"); + }); + + it("should update hidden span text when selection changes", () => { + cy.mount( + + ); + + // Verify initial hidden span text + cy.get("#selectUpdateHiddenText") + .shadow() + .find("[id$='-selectedOptionText']") + .should("have.text", "Second Option"); + + // Open select and change selection + cy.get("#selectUpdateHiddenText") + .realClick(); + + cy.get("#selectUpdateHiddenText") + .should("have.attr", "opened"); + + // Select third option + cy.get("#selectUpdateHiddenText") + .find("[ui5-option]") + .eq(2) + .realClick(); + + // Verify hidden span text is updated for screen reader announcement + cy.get("#selectUpdateHiddenText") + .shadow() + .find("[id$='-selectedOptionText']") + .should("have.text", "Third Option"); + }); + + it("should set ariaActiveDescendantElement on focus ref when popover closes after selection change (NVDA fix)", () => { + cy.mount( + + ); + + // Open select + cy.get("#selectNvdaFix") + .realClick(); + + cy.get("#selectNvdaFix") + .should("have.attr", "opened"); + + // Select a different option + cy.get("#selectNvdaFix") + .find("[ui5-option]") + .eq(2) + .realClick(); + + // After popover closes, verify the hidden span contains the new selection + cy.get("#selectNvdaFix") + .should("not.have.attr", "opened"); + + cy.get("#selectNvdaFix") + .shadow() + .find("[id$='-selectedOptionText']") + .should("have.text", "Gamma"); + + // The ariaActiveDescendantElement is set temporarily and cleared after 100ms + // We verify the mechanism works by checking the hidden span has the correct content + // which is what screen readers like NVDA will announce + cy.get("#selectNvdaFix") + .shadow() + .find("[id$='-selectedOptionText']") + .should("have.attr", "role", "option") + .should("have.attr", "aria-selected", "true"); + }); }); describe("Select - Popover", () => { diff --git a/packages/main/src/Select.ts b/packages/main/src/Select.ts index 2aa8a642ae2c..56c1ec690d76 100644 --- a/packages/main/src/Select.ts +++ b/packages/main/src/Select.ts @@ -953,13 +953,29 @@ class Select extends UI5Element implements IFormInputElement { this._iconPressed = false; this._listWidth = 0; + const selectionChanged = this._lastSelectedOption !== this.options[this._selectedIndex]; + if (this._escapePressed) { this._select(this._selectedIndexBeforeOpen); this._escapePressed = false; - } else if (this._lastSelectedOption !== this.options[this._selectedIndex]) { + } else if (selectionChanged) { this._fireChangeEvent(this.options[this._selectedIndex]); this._lastSelectedOption = this.options[this._selectedIndex]; } + + // Use ariaActiveDescendantElement to trigger screen readers to read the updated value + // Reference the hidden span in our shadow root that contains the selected text + if (selectionChanged) { + const focusRef = this.getFocusDomRef() as HTMLElement; + const hiddenOptionSpan = this.shadowRoot?.querySelector(`#${this._id}-selectedOptionText`); + if (focusRef && hiddenOptionSpan && !this._isPickerOpen) { + (focusRef as any).ariaActiveDescendantElement = hiddenOptionSpan; + // Clear it after a short delay to avoid JAWS VPC mode + setTimeout(() => { + (focusRef as any).ariaActiveDescendantElement = null; + }, 100); + } + } this.fireDecoratorEvent("close"); } diff --git a/packages/main/src/SelectTemplate.tsx b/packages/main/src/SelectTemplate.tsx index c0ed1fb24c7c..c0410eedabbe 100644 --- a/packages/main/src/SelectTemplate.tsx +++ b/packages/main/src/SelectTemplate.tsx @@ -91,6 +91,10 @@ export default function SelectTemplate(this: Select) { {this.ariaDescriptionText} } + {/* Hidden element for ariaActiveDescendantElement reference */} + + {this.text} + {SelectPopoverTemplate.call(this)}