Skip to content
Open
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
127 changes: 127 additions & 0 deletions packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<UserSettingsDialog open>
<UserSettingsItem text="Appearance" selected>
<UserSettingsView text="Setting1"></UserSettingsView>
</UserSettingsItem>
<UserSettingsItem text="Language">
<UserSettingsView text="Setting2"></UserSettingsView>
</UserSettingsItem>
</UserSettingsDialog>);
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(<UserSettingsDialog open>
<UserSettingsItem text="Appearance" selected>
<UserSettingsView text="Setting1"></UserSettingsView>
</UserSettingsItem>
</UserSettingsDialog>);
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(<UserSettingsDialog open>
<UserSettingsItem text="Appearance" selected>
<UserSettingsView text="Setting1"></UserSettingsView>
</UserSettingsItem>
<UserSettingsItem text="Language">
<UserSettingsView text="Setting2"></UserSettingsView>
</UserSettingsItem>
</UserSettingsDialog>);
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(<UserSettingsDialog open>
<UserSettingsItem text="Appearance" selected>
<UserSettingsView text="Setting1"></UserSettingsView>
</UserSettingsItem>
</UserSettingsDialog>);
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(<UserSettingsDialog open>
<UserSettingsItem text="Appearance" selected>
<UserSettingsView text="Setting1"></UserSettingsView>
</UserSettingsItem>
<UserSettingsItem text="Language">
<UserSettingsView text="Setting2"></UserSettingsView>
</UserSettingsItem>
</UserSettingsDialog>);
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(<UserSettingsDialog open>
<UserSettingsItem text="Appearance">
<UserSettingsAppearanceView text="Themes">
<UserSettingsAppearanceViewItem item-key="sap_horizon" text="SAP Morning Horizon" icon="palette" selected></UserSettingsAppearanceViewItem>
<UserSettingsAppearanceViewGroup header-text="SAP Quartz">
<UserSettingsAppearanceViewItem item-key="sap_fiori_3" text="SAP Quartz Light" icon="palette"></UserSettingsAppearanceViewItem>
</UserSettingsAppearanceViewGroup>
</UserSettingsAppearanceView>
</UserSettingsItem>
</UserSettingsDialog>);
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(<UserSettingsDialog open>
<UserSettingsItem text="Appearance">
<UserSettingsAppearanceView text="Themes">
<UserSettingsAppearanceViewItem item-key="sap_horizon" text="SAP Morning Horizon" icon="palette" selected></UserSettingsAppearanceViewItem>
</UserSettingsAppearanceView>
</UserSettingsItem>
</UserSettingsDialog>);
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(<UserSettingsDialog open>
Expand Down
13 changes: 13 additions & 0 deletions packages/fiori/src/UserSettingsAppearanceView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -93,6 +101,7 @@ class UserSettingsAppearanceView extends UserSettingsView {
_handleItemClick = (e: CustomEvent<ListItemClickEventDetail>) => {
const listItem = e.detail.item as ListItemBase & { associatedSettingItem?: UserSettingsAppearanceViewItem };
if (isInstanceOfUserSettingsAppearanceViewItem(listItem)) {
const alreadySelected = listItem.selected;
const eventPrevented = !this.fireDecoratorEvent("selection-change", {
item: listItem,
});
Expand All @@ -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);
}
}
}
};
Expand Down
8 changes: 8 additions & 0 deletions packages/fiori/src/UserSettingsAppearanceViewItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/fiori/src/UserSettingsDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 = {
Expand Down Expand Up @@ -286,6 +289,7 @@ class UserSettingsDialog extends UI5Element {
_handleItemClick(e: CustomEvent<ListItemClickEventDetail>) {
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,
});
Expand All @@ -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);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/fiori/src/UserSettingsDialogTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function renderList(this: UserSettingsDialog, items: Array<UserSettingsItem> = [
tooltip={item._tooltip}
ref={this.captureRef.bind(item)}
selected={item.selected}
_forceAriaSelected
disabled={item.disabled}
accessibleName={item.ariaLabelledByText}
type={this._showSettingWithNavigation ? "Navigation" : "Active"}
Expand Down
5 changes: 4 additions & 1 deletion packages/fiori/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
12 changes: 11 additions & 1 deletion packages/main/src/ListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

Expand Down
Loading