diff --git a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx index a35c40b517632..0c4f0f24b190f 100644 --- a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx +++ b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx @@ -1538,6 +1538,176 @@ describe("Appearance view", () => { }); }); +describe("Selection accessibility", () => { + it("exposes selected state via aria-selected and hidden describedby text on dialog items", () => { + cy.mount( + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").shadow().find("[ui5-dialog]").find("[ui5-li]").as("items"); + + cy.get("@items").first().shadow().find("li").should("have.attr", "aria-selected", "true"); + cy.get("@items").first().shadow().find(".ui5-hidden-text").should("contain.text", "Selected"); + cy.get("@items").last().shadow().find("li").should("have.attr", "aria-selected", "false"); + cy.get("@items").last().shadow().find(".ui5-hidden-text").should("contain.text", "Not Selected"); + }); + + it("does not render a radio button in dialog items (selection-mode Single)", () => { + cy.mount( + + + + ); + cy.get("[ui5-user-settings-dialog]").shadow().find("[ui5-li]").first() + .shadow().find("[ui5-radio-button]").should("not.exist"); + }); + + it("announces 'Selected' when a different dialog item is selected", () => { + cy.mount( + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get(".ui5-invisiblemessage-polite").as("liveRegion").should("have.text", ""); + + cy.get("@settings").shadow().find("[ui5-dialog]").find("[ui5-li]").last().click(); + + cy.get("@liveRegion").should("contain.text", "Selected"); + }); + + it("does not announce when the already-selected dialog item is clicked", () => { + cy.mount( + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get(".ui5-invisiblemessage-polite").as("liveRegion").should("have.text", ""); + + cy.get("@settings").shadow().find("[ui5-dialog]").find("[ui5-li]").first().click(); + + cy.get("@liveRegion").should("have.text", ""); + }); + + it("selects and announces on Space/Enter", () => { + cy.mount( + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get(".ui5-invisiblemessage-polite").as("liveRegion").should("have.text", ""); + + cy.get("@settings").shadow().find("[ui5-dialog]").find("[ui5-li]").last().as("secondItem"); + + // Move focus to the non-selected item and select it with the keyboard. + cy.get("@secondItem").focus(); + cy.realPress("Enter"); + cy.get("@secondItem").shadow().find("li").should("have.attr", "aria-selected", "true"); + cy.get("@liveRegion").should("contain.text", "Selected"); + }); + + it("arrow keys move focus without changing selection", () => { + cy.mount( + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").shadow().find("[ui5-dialog]").find("[ui5-li]").first().as("firstItem"); + cy.get("@settings").shadow().find("[ui5-dialog]").find("[ui5-li]").last().as("secondItem"); + + cy.get("@firstItem").focus(); + cy.realPress("ArrowDown"); + + // Focus moved to the second item, but selection stays on the first (Single, not SingleAuto). + cy.get("@secondItem").should("be.focused"); + cy.get("@firstItem").shadow().find("li").should("have.attr", "aria-selected", "true"); + cy.get("@secondItem").shadow().find("li").should("have.attr", "aria-selected", "false"); + }); + + it("does not announce when selection-change on the dialog is prevented", () => { + cy.mount( + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").then($settings => { + $settings.get(0).addEventListener("selection-change", (e: Event) => e.preventDefault()); + }); + cy.get(".ui5-invisiblemessage-polite").as("liveRegion").should("have.text", ""); + + cy.get("@settings").shadow().find("[ui5-dialog]").find("[ui5-li]").last().click(); + + cy.get("@liveRegion").should("have.text", ""); + }); + + it("exposes selected state on appearance view items and announces on change", () => { + cy.mount( + + + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").as("items"); + + // Selected theme exposes "Selected"; the grouped one exposes "Not Selected". + cy.get("@items").first().shadow().find("li").should("have.attr", "aria-selected", "true"); + cy.get("@items").first().shadow().find(".ui5-hidden-text").should("contain.text", "Selected"); + cy.get("@items").eq(1).shadow().find(".ui5-hidden-text").should("contain.text", "Not Selected"); + + // No radio button is rendered - selection mode Single places no selection control. + cy.get("@items").first().shadow().find("[ui5-radio-button]").should("not.exist"); + + // Selecting a different theme announces "Selected". + cy.get(".ui5-invisiblemessage-polite").as("liveRegion").should("have.text", ""); + cy.get("@items").eq(1).click(); + cy.get("@liveRegion").should("contain.text", "Selected"); + }); + + it("does not announce when the already-selected appearance item is clicked", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + cy.get(".ui5-invisiblemessage-polite").as("liveRegion").should("have.text", ""); + + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").first().click(); + + cy.get("@liveRegion").should("have.text", ""); + }); +}); + describe("F6 Navigation", () => { it("tests host has fastnavgroup-container attribute", () => { cy.mount( diff --git a/packages/fiori/src/UserSettingsAppearanceView.ts b/packages/fiori/src/UserSettingsAppearanceView.ts index 9c23b41056787..370538b27052e 100644 --- a/packages/fiori/src/UserSettingsAppearanceView.ts +++ b/packages/fiori/src/UserSettingsAppearanceView.ts @@ -5,7 +5,7 @@ import type UserSettingsAppearanceViewItem from "./UserSettingsAppearanceViewIte import { isInstanceOfUserSettingsAppearanceViewItem } from "./UserSettingsAppearanceViewItem.js"; import type UserSettingsAppearanceViewGroup from "./UserSettingsAppearanceViewGroup.js"; import { isInstanceOfUserSettingsAppearanceViewGroup } from "./UserSettingsAppearanceViewGroup.js"; -import type { ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js"; +import type { ListSelectionChangeEventDetail } from "@ui5/webcomponents/dist/List.js"; import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js"; import { @@ -90,18 +90,18 @@ class UserSettingsAppearanceView extends UserSettingsView { return allItems; } - _handleItemClick = (e: CustomEvent) => { - const listItem = e.detail.item as ListItemBase & { associatedSettingItem?: UserSettingsAppearanceViewItem }; + _handleSelectionChange = (e: CustomEvent) => { + const listItem = e.detail.targetItem as ListItemBase & { associatedSettingItem?: UserSettingsAppearanceViewItem }; if (isInstanceOfUserSettingsAppearanceViewItem(listItem)) { + // The inner list runs in selectionMode="Single", so it already owns the + // item's selected state and provides the accessibility announcement. const eventPrevented = !this.fireDecoratorEvent("selection-change", { item: listItem, }); - if (!eventPrevented) { - this._getAllItems().forEach(viewItem => { - viewItem.selected = false; - }); - listItem.selected = true; + if (eventPrevented) { + // Revert the list selection so it stays in sync with the model. + e.preventDefault(); } } }; diff --git a/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx b/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx index 276e910a655fc..ba0f18c4ed008 100644 --- a/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx +++ b/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx @@ -7,7 +7,7 @@ export default function UserSettingsAppearanceViewTemplate(this: UserSettingsApp