diff --git a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx
index 9db3e7df18d4..2a90af3460f8 100644
--- a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx
+++ b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx
@@ -1451,6 +1451,133 @@ 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 (stays selection-mode None)", () => {
+ 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("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 is still None.
+ 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 9c23b4105678..ffa5074f7a4e 100644
--- a/packages/fiori/src/UserSettingsAppearanceView.ts
+++ b/packages/fiori/src/UserSettingsAppearanceView.ts
@@ -11,8 +11,13 @@ import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js";
import {
customElement, slotStrict as slot, eventStrict as event,
} from "@ui5/webcomponents-base/dist/decorators.js";
+import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
+import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
+import announce from "@ui5/webcomponents-base/dist/util/InvisibleMessage.js";
+import InvisibleMessageMode from "@ui5/webcomponents-base/dist/types/InvisibleMessageMode.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import type { DefaultSlot, Slot } from "@ui5/webcomponents-base/dist/UI5Element.js";
+import { USER_SETTINGS_LIST_ITEM_SELECTED } from "./generated/i18n/i18n-defaults.js";
type UserSettingsAppearanceViewItemSelectEventDetail = {
item: UserSettingsAppearanceViewItem;
@@ -49,6 +54,9 @@ type UserSettingsAppearanceViewItemSelectEventDetail = {
* @since 2.17.0
*/
class UserSettingsAppearanceView extends UserSettingsView {
+ @i18n("@ui5/webcomponents-fiori")
+ static i18nBundle: I18nBundle;
+
eventDetails!: {
"selection-change": UserSettingsAppearanceViewItemSelectEventDetail;
}
@@ -93,6 +101,7 @@ class UserSettingsAppearanceView extends UserSettingsView {
_handleItemClick = (e: CustomEvent) => {
const listItem = e.detail.item as ListItemBase & { associatedSettingItem?: UserSettingsAppearanceViewItem };
if (isInstanceOfUserSettingsAppearanceViewItem(listItem)) {
+ const alreadySelected = listItem.selected;
const eventPrevented = !this.fireDecoratorEvent("selection-change", {
item: listItem,
});
@@ -102,6 +111,10 @@ class UserSettingsAppearanceView extends UserSettingsView {
viewItem.selected = false;
});
listItem.selected = true;
+
+ if (!alreadySelected) {
+ announce(UserSettingsAppearanceView.i18nBundle.getText(USER_SETTINGS_LIST_ITEM_SELECTED), InvisibleMessageMode.Polite);
+ }
}
}
};
diff --git a/packages/fiori/src/UserSettingsAppearanceViewItem.ts b/packages/fiori/src/UserSettingsAppearanceViewItem.ts
index bc7073402a9f..b7dcd3e7aa86 100644
--- a/packages/fiori/src/UserSettingsAppearanceViewItem.ts
+++ b/packages/fiori/src/UserSettingsAppearanceViewItem.ts
@@ -69,6 +69,14 @@ class UserSettingsAppearanceViewItem extends ListItemCustom {
@property()
colorScheme = "Accent7";
+ /**
+ * Exposes the item's `selected` state to assistive technology, since these
+ * items are always single-select theme options managed by the parent view.
+ * @private
+ */
+ @property({ type: Boolean })
+ _forceAriaSelected = true;
+
get isUserSettingsAppearanceViewItem(): boolean {
return true;
}
diff --git a/packages/fiori/src/UserSettingsDialog.ts b/packages/fiori/src/UserSettingsDialog.ts
index 3edd22591a81..de6f67dfe274 100644
--- a/packages/fiori/src/UserSettingsDialog.ts
+++ b/packages/fiori/src/UserSettingsDialog.ts
@@ -13,6 +13,8 @@ import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js";
import type { PopupBeforeCloseEventDetail } from "@ui5/webcomponents/dist/Popup.js";
import { isPhone, isTablet, isCombi } from "@ui5/webcomponents-base/dist/Device.js";
import MediaRange from "@ui5/webcomponents-base/dist/MediaRange.js";
+import announce from "@ui5/webcomponents-base/dist/util/InvisibleMessage.js";
+import InvisibleMessageMode from "@ui5/webcomponents-base/dist/types/InvisibleMessageMode.js";
import UserSettingsDialogTemplate from "./UserSettingsDialogTemplate.js";
import type UserSettingsItem from "./UserSettingsItem.js";
import UserSettingsDialogCss from "./generated/themes/UserSettingsDialog.css.js";
@@ -25,6 +27,7 @@ import {
USER_SETTINGS_DIALOG_SAVE_BUTTON_TEXT,
USER_SETTINGS_DIALOG_CANCEL_BUTTON_TEXT,
USER_SETTINGS_DIALOG_NO_SEARCH_RESULTS_TEXT,
+ USER_SETTINGS_LIST_ITEM_SELECTED,
} from "./generated/i18n/i18n-defaults.js";
type UserSettingsItemSelectEventDetail = {
@@ -286,6 +289,7 @@ class UserSettingsDialog extends UI5Element {
_handleItemClick(e: CustomEvent) {
const setting = e.detail.item as ListItemBase & { associatedSettingItem: UserSettingsItem };
const settingItem = setting.associatedSettingItem;
+ const alreadySelected = settingItem.selected;
const eventPrevented = !this.fireDecoratorEvent("selection-change", {
item: settingItem,
});
@@ -299,6 +303,10 @@ class UserSettingsDialog extends UI5Element {
item.selected = false;
});
settingItem.selected = true;
+
+ if (!alreadySelected) {
+ announce(UserSettingsDialog.i18nBundle.getText(USER_SETTINGS_LIST_ITEM_SELECTED), InvisibleMessageMode.Polite);
+ }
}
}
diff --git a/packages/fiori/src/UserSettingsDialogTemplate.tsx b/packages/fiori/src/UserSettingsDialogTemplate.tsx
index e3e7a8b291e0..015fa859b495 100644
--- a/packages/fiori/src/UserSettingsDialogTemplate.tsx
+++ b/packages/fiori/src/UserSettingsDialogTemplate.tsx
@@ -76,6 +76,7 @@ function renderList(this: UserSettingsDialog, items: Array = [
tooltip={item._tooltip}
ref={this.captureRef.bind(item)}
selected={item.selected}
+ _forceAriaSelected
disabled={item.disabled}
accessibleName={item.ariaLabelledByText}
type={this._showSettingWithNavigation ? "Navigation" : "Active"}
diff --git a/packages/fiori/src/i18n/messagebundle.properties b/packages/fiori/src/i18n/messagebundle.properties
index 7a7d505ed443..f97e60564246 100644
--- a/packages/fiori/src/i18n/messagebundle.properties
+++ b/packages/fiori/src/i18n/messagebundle.properties
@@ -621,4 +621,7 @@ USER_SETTINGS_DIALOG_NO_SEARCH_RESULTS_TEXT=No search results
USER_SETTINGS_ACCOUNT_EDIT_AVATAR_TXT=Edit avatar
#XTXT: User settings account view manage button
-USER_SETTINGS_ACCOUNT_MANAGE_ACCOUNT_BUTTON_TXT=Manage account
\ No newline at end of file
+USER_SETTINGS_ACCOUNT_MANAGE_ACCOUNT_BUTTON_TXT=Manage account
+
+#XACT: ARIA announcement when a user settings list item becomes selected
+USER_SETTINGS_LIST_ITEM_SELECTED=Selected
\ No newline at end of file
diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts
index 668619999151..5aea5260dfc9 100644
--- a/packages/main/src/ListItem.ts
+++ b/packages/main/src/ListItem.ts
@@ -194,6 +194,16 @@ abstract class ListItem extends ListItemBase {
@property()
_forcedAccessibleRole?: string;
+ /**
+ * Forces the item to expose its `selected` state to assistive technology
+ * (hidden "Selected"/"Not Selected" describedby text) even when the parent
+ * list is not in a selection mode. Used by components that manage selection
+ * manually, e.g. `ui5-user-settings-dialog`.
+ * @private
+ */
+ @property({ type: Boolean })
+ _forceAriaSelected = false;
+
@property({ noAttribute: true })
_inheritedAccessibleRole?: string;
@@ -450,7 +460,7 @@ abstract class ListItem extends ListItemBase {
}
get _ariaSelected() {
- if (this.modeMultiple || this.modeSingleSelect) {
+ if (this.modeMultiple || this.modeSingleSelect || this._forceAriaSelected) {
return this.selected;
}