From 8676ac103cd47fa4dfa7224b117b652c52b1a689 Mon Sep 17 00:00:00 2001 From: Svilen Darvenyashki Date: Sun, 30 Nov 2025 15:37:40 +0200 Subject: [PATCH 1/6] feat(ui5-user-settings): add UserSettingsAppearanceView components --- packages/fiori/README.md | 81 +-- .../cypress/specs/UserSettingsDialog.cy.tsx | 462 ++++++++++++++++++ .../fiori/src/UserSettingsAppearanceView.ts | 114 +++++ .../src/UserSettingsAppearanceViewGroup.ts | 40 ++ .../src/UserSettingsAppearanceViewItem.ts | 70 +++ ...UserSettingsAppearanceViewItemTemplate.tsx | 24 + .../UserSettingsAppearanceViewTemplate.tsx | 15 + packages/fiori/src/bundle.esm.ts | 3 + .../themes/UserSettingsAppearanceViewItem.css | 65 +++ .../fiori/test/pages/UserSettingsDialog.html | 53 +- 10 files changed, 863 insertions(+), 64 deletions(-) create mode 100644 packages/fiori/src/UserSettingsAppearanceView.ts create mode 100644 packages/fiori/src/UserSettingsAppearanceViewGroup.ts create mode 100644 packages/fiori/src/UserSettingsAppearanceViewItem.ts create mode 100644 packages/fiori/src/UserSettingsAppearanceViewItemTemplate.tsx create mode 100644 packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx create mode 100644 packages/fiori/src/themes/UserSettingsAppearanceViewItem.css diff --git a/packages/fiori/README.md b/packages/fiori/README.md index 4ebb5385c0f8..6c02707f6e36 100644 --- a/packages/fiori/README.md +++ b/packages/fiori/README.md @@ -7,46 +7,47 @@ such as a common header (ShellBar). ## Provided components -| Web Component | Tag name | Module import | -|-------------------------------------------|----------------------------------|-----------------------------------------------------------------------| -| Barcode Scanner Dialog | `ui5-barcode-scanner-dialog` | `import "@ui5/webcomponents-fiori/dist/BarcodeScannerDialog.js";` | -| Dynamic Side Content | `ui5-dynamic-side-content` | `import "@ui5/webcomponents-fiori/dist/DynamicSideContent.js";` | -| Flexible Column Layout | `ui5-flexible-column-layout` | `import "@ui5/webcomponents-fiori/dist/FlexibleColumnLayout.js";` | -| Illustrated Message | `ui5-illustrated-message` | `import "@ui5/webcomponents-fiori/dist/IllustratedMessage.js";` | -| Media Gallery | `ui5-media-gallery` | `import "@ui5/webcomponents-fiori/dist/MediaGallery.js";` | -| Media Gallery Item | `ui5-media-gallery-item` | comes with `ui5-media-gallery` | -| Notification List | `ui5-notification-list` | `import "@ui5/webcomponents-fiori/dist/NotifcationList.js";` | -| Notification List Item | `ui5-li-notification` | `import "@ui5/webcomponents-fiori/dist/NotifcationListItem.js";` | -| Notification Group List Item | `ui5-li-notification-group` | `import "@ui5/webcomponents-fiori/dist/NotifcationListGroupItem.js";` | -| Notification Action | `ui5-notification-action` | `import "@ui5/webcomponents-fiori/dist/NotificationAction.js";` | -| Page | `ui5-page` | `import "@ui5/webcomponents-fiori/dist/Page.js";` | -| Product Switch | `ui5-product-switch` | `import "@ui5/webcomponents-fiori/dist/ProductSwitch.js";` | -| Product Switch Item | `ui5-product-switch-item` | `import "@ui5/webcomponents-fiori/dist/ProductSwitchItem.js";` | -| Shell Bar | `ui5-shellbar` | `import "@ui5/webcomponents-fiori/dist/ShellBar.js";` | -| Shell Bar Item | `ui5-shellbar-item` | `import "@ui5/webcomponents-fiori/dist/ShellBarItem.js";` | -| Side Navigation | `ui5-side-navigation` | `import "@ui5/webcomponents-fiori/dist/SideNavigation.js";` | -| Side Navigation Item | `ui5-side-navigation-item` | `import "@ui5/webcomponents-fiori/dist/SideNavigationItem.js";` | -| Side Navigation Sub Item | `ui5-side-navigation-sub-item` | `import "@ui5/webcomponents-fiori/dist/SideNavigationSubItem.js";` | -| Side Navigation Group | `ui5-side-navigation-group` | `import "@ui5/webcomponents-fiori/dist/SideNavigationGroup.js";` | -| Timeline | `ui5-timeline` | `import "@ui5/webcomponents-fiori/dist/Timeline.js";` | -| Timeline Item | `ui5-timeline-item` | comes with `ui5-timeline` | -| Timeline Group Item | `ui5-timeline-group-item` | `import "@ui5/webcomponents-fiori/dist/TimelineGroupItem.js";` | -| Upload Collection | `ui5-upload-collection` | `import "@ui5/webcomponents-fiori/dist/UploadCollection.js";` | -| Upload Collection Item | `ui5-upload-collection-item` | `import "@ui5/webcomponents-fiori/dist/UploadCollectionItem.js";` | -| User Menu | `ui5-user-menu` | `import "@ui5/webcomponents-fiori/dist/UserMenu.js";` | -| User Menu Account | `ui5-user-menu-account` | `import "@ui5/webcomponents-fiori/dist/UserMenuAccount.js";` | -| User Menu Item | `ui5-user-menu-item` | `import "@ui5/webcomponents-fiori/dist/UserMenuItem.js";` | -| User Menu Item Group | `ui5-user-menu-item-group` | `import "@ui5/webcomponents-fiori/dist/UserMenuItemGroup.js";` | -| User Settings Dialog | `ui5-user-settings-dialog` | `import "@ui5/webcomponents-fiori/dist/UserSettingsDialog.js";` | -| User Settings Item | `ui5-user-settings-item` | `import "@ui5/webcomponents-fiori/dist/UserSettingsItem.js";` | -| User Settings Account View | `ui5-user-settings-account-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsAccountView.js";` | -| User Settings View | `ui5-user-settings-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsView.js";` | -| View Settings Dialog | `ui5-view-settings-dialog` | `import "@ui5/webcomponents-fiori/dist/ViewSettingsDialog.js";` | -| View Settings Dialog - Sort Item | `ui5-sort-item` | `import "@ui5/webcomponents-fiori/dist/SortItem.js";` | -| View Settings Dialog - Filter Item | `ui5-filter-item` | `import "@ui5/webcomponents-fiori/dist/FilterItem.js";` | -| View Settings Dialog - Filter Item Option | `ui5-filter-item-option` | `import "@ui5/webcomponents-fiori/dist/FilterItemOption.js";` | -| Wizard | `ui5-wizard` | `import "@ui5/webcomponents-fiori/dist/Wizard.js";` | -| Wizard Step | `ui5-wizard-step` | comes with `ui5-wizard` | +| Web Component | Tag name | Module import | +|-------------------------------------------|-----------------------------------------|------------------------------------------------------------------------| +| Barcode Scanner Dialog | `ui5-barcode-scanner-dialog` | `import "@ui5/webcomponents-fiori/dist/BarcodeScannerDialog.js";` | +| Dynamic Side Content | `ui5-dynamic-side-content` | `import "@ui5/webcomponents-fiori/dist/DynamicSideContent.js";` | +| Flexible Column Layout | `ui5-flexible-column-layout` | `import "@ui5/webcomponents-fiori/dist/FlexibleColumnLayout.js";` | +| Illustrated Message | `ui5-illustrated-message` | `import "@ui5/webcomponents-fiori/dist/IllustratedMessage.js";` | +| Media Gallery | `ui5-media-gallery` | `import "@ui5/webcomponents-fiori/dist/MediaGallery.js";` | +| Media Gallery Item | `ui5-media-gallery-item` | comes with `ui5-media-gallery` | +| Notification List | `ui5-notification-list` | `import "@ui5/webcomponents-fiori/dist/NotifcationList.js";` | +| Notification List Item | `ui5-li-notification` | `import "@ui5/webcomponents-fiori/dist/NotifcationListItem.js";` | +| Notification Group List Item | `ui5-li-notification-group` | `import "@ui5/webcomponents-fiori/dist/NotifcationListGroupItem.js";` | +| Notification Action | `ui5-notification-action` | `import "@ui5/webcomponents-fiori/dist/NotificationAction.js";` | +| Page | `ui5-page` | `import "@ui5/webcomponents-fiori/dist/Page.js";` | +| Product Switch | `ui5-product-switch` | `import "@ui5/webcomponents-fiori/dist/ProductSwitch.js";` | +| Product Switch Item | `ui5-product-switch-item` | `import "@ui5/webcomponents-fiori/dist/ProductSwitchItem.js";` | +| Shell Bar | `ui5-shellbar` | `import "@ui5/webcomponents-fiori/dist/ShellBar.js";` | +| Shell Bar Item | `ui5-shellbar-item` | `import "@ui5/webcomponents-fiori/dist/ShellBarItem.js";` | +| Side Navigation | `ui5-side-navigation` | `import "@ui5/webcomponents-fiori/dist/SideNavigation.js";` | +| Side Navigation Item | `ui5-side-navigation-item` | `import "@ui5/webcomponents-fiori/dist/SideNavigationItem.js";` | +| Side Navigation Sub Item | `ui5-side-navigation-sub-item` | `import "@ui5/webcomponents-fiori/dist/SideNavigationSubItem.js";` | +| Side Navigation Group | `ui5-side-navigation-group` | `import "@ui5/webcomponents-fiori/dist/SideNavigationGroup.js";` | +| Timeline | `ui5-timeline` | `import "@ui5/webcomponents-fiori/dist/Timeline.js";` | +| Timeline Item | `ui5-timeline-item` | comes with `ui5-timeline` | +| Timeline Group Item | `ui5-timeline-group-item` | `import "@ui5/webcomponents-fiori/dist/TimelineGroupItem.js";` | +| Upload Collection | `ui5-upload-collection` | `import "@ui5/webcomponents-fiori/dist/UploadCollection.js";` | +| Upload Collection Item | `ui5-upload-collection-item` | `import "@ui5/webcomponents-fiori/dist/UploadCollectionItem.js";` | +| User Menu | `ui5-user-menu` | `import "@ui5/webcomponents-fiori/dist/UserMenu.js";` | +| User Menu Account | `ui5-user-menu-account` | `import "@ui5/webcomponents-fiori/dist/UserMenuAccount.js";` | +| User Menu Item | `ui5-user-menu-item` | `import "@ui5/webcomponents-fiori/dist/UserMenuItem.js";` | +| User Menu Item Group | `ui5-user-menu-item-group` | `import "@ui5/webcomponents-fiori/dist/UserMenuItemGroup.js";` | +| User Settings Dialog | `ui5-user-settings-dialog` | `import "@ui5/webcomponents-fiori/dist/UserSettingsDialog.js";` | +| User Settings Item | `ui5-user-settings-item` | `import "@ui5/webcomponents-fiori/dist/UserSettingsItem.js";` | +| User Settings Account View | `ui5-user-settings-account-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsAccountView.js";` | +| User Settings Appearance View | `ui5-user-settings-appearance-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceView.js";`| +| User Settings View | `ui5-user-settings-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsView.js";` | +| View Settings Dialog | `ui5-view-settings-dialog` | `import "@ui5/webcomponents-fiori/dist/ViewSettingsDialog.js";` | +| View Settings Dialog - Sort Item | `ui5-sort-item` | `import "@ui5/webcomponents-fiori/dist/SortItem.js";` | +| View Settings Dialog - Filter Item | `ui5-filter-item` | `import "@ui5/webcomponents-fiori/dist/FilterItem.js";` | +| View Settings Dialog - Filter Item Option | `ui5-filter-item-option` | `import "@ui5/webcomponents-fiori/dist/FilterItemOption.js";` | +| Wizard | `ui5-wizard` | `import "@ui5/webcomponents-fiori/dist/Wizard.js";` | +| Wizard Step | `ui5-wizard-step` | comes with `ui5-wizard` | ## Provided assets diff --git a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx index d135f665d782..763c6d25f2cf 100644 --- a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx +++ b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx @@ -1,6 +1,9 @@ import UserSettingsItem from "../../src/UserSettingsItem.js"; import UserSettingsView from "../../src/UserSettingsView.js"; import UserSettingsAccountView from "../../src/UserSettingsAccountView.js"; +import UserSettingsAppearanceView from "../../src/UserSettingsAppearanceView.js"; +import UserSettingsAppearanceViewItem from "../../src/UserSettingsAppearanceViewItem.js"; +import UserSettingsAppearanceViewGroup from "../../src/UserSettingsAppearanceViewGroup.js"; import UserMenuAccount from "../../src/UserMenuAccount.js"; import UserSettingsDialog from "../../src/UserSettingsDialog.js"; import Button from "@ui5/webcomponents/dist/Button.js"; @@ -905,3 +908,462 @@ describe("User account view", () => { cy.get("@clicked").should("have.been.calledOnce"); }); }); + +describe("Appearance view", () => { + it("tests appearance view no config", () => { + cy.mount( + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").should("exist"); + cy.get("@settings").find("[ui5-user-settings-item]").as("settingItem"); + cy.get("@settingItem").should("exist"); + cy.get("@settingItem").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + cy.get("@appearanceView").should("exist"); + }); + + it("tests appearance view text", () => { + cy.mount( + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").should("exist"); + cy.get("@settings").find("[ui5-user-settings-item]").as("settingItem"); + cy.get("@settingItem").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + cy.get("@appearanceView").should("exist"); + cy.get("@appearanceView").should("have.attr", "text", "Themes"); + cy.get("@settingItem").shadow().find("[ui5-tabcontainer]").should("not.exist"); + }); + + it("tests appearance view items exist", () => { + cy.mount( + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").should("exist"); + cy.get("@settings").find("[ui5-user-settings-item]").as("settingItem"); + cy.get("@settingItem").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + cy.get("@appearanceView").should("exist"); + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").as("items"); + cy.get("@items").should("have.length", 2); + }); + + it("tests appearance view item properties", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").should("exist"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").as("item"); + cy.get("@item").should("exist"); + cy.get("@item").should("have.attr", "text", "SAP Morning Horizon"); + cy.get("@item").should("have.attr", "icon", "palette"); + cy.get("@item").should("have.attr", "color-scheme", "Accent5"); + cy.get("@item").then($item => { + const item = $item.get(0) as any; + expect(item.itemKey).to.equal("sap_horizon"); + expect(item.text).to.equal("SAP Morning Horizon"); + expect(item.icon).to.equal("palette"); + expect(item.colorScheme).to.equal("Accent5"); + }); + }); + + it("tests appearance view item default icon", () => { + 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("item"); + cy.get("@item").then($item => { + const item = $item.get(0) as any; + expect(item.icon).to.equal("product"); + }); + }); + + it("tests appearance view item default color-scheme", () => { + 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("item"); + cy.get("@item").then($item => { + const item = $item.get(0) as any; + expect(item.colorScheme).to.equal("Accent7"); + }); + }); + + it("tests appearance view item custom color-scheme", () => { + 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("item"); + cy.get("@item").shadow().find("[ui5-avatar]").as("avatar"); + cy.get("@avatar").should("have.attr", "color-scheme", "Accent3"); + }); + + it("tests appearance view item avatar", () => { + 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("item"); + cy.get("@item").shadow().find("[ui5-avatar]").as("avatar"); + cy.get("@avatar").should("exist"); + cy.get("@avatar").should("have.length", 1); + cy.get("@avatar").should("have.attr", "icon", "palette"); + cy.get("@avatar").should("have.attr", "shape", "Square"); + cy.get("@avatar").should("have.attr", "color-scheme", "Accent7"); + }); + + it("tests appearance view item text displayed", () => { + 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("item"); + cy.get("@item").shadow().find(".item-title").as("title"); + cy.get("@title").should("exist"); + cy.get("@title").contains("SAP Morning Horizon"); + }); + + it("tests appearance view with groups", () => { + 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-group]").should("have.length", 2); + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").should("have.length", 3); + }); + + it("tests appearance view group header-text", () => { + 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-group]").as("group"); + cy.get("@group").should("have.attr", "header-text", "SAP Horizon"); + cy.get("@group").shadow().find("[ui5-li-group-header]").contains("SAP Horizon"); + }); + + it("tests appearance view mixed grouped and ungrouped items", () => { + 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-group]").should("have.length", 1); + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").should("have.length", 2); + }); + + it("tests theme-selected event", () => { + cy.mount( + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + + cy.get("@appearanceView").then($view => { + $view.get(0).addEventListener("theme-selected", cy.stub().as("themeSelected")); + }); + + cy.get("@appearanceView") + .find("[ui5-user-settings-appearance-view-item]") + .first() + .click(); + + cy.get("@themeSelected").should("have.been.calledOnce"); + }); + + it("tests theme-selected event detail contains correct key", () => { + cy.mount( + + + + + + + ); + + // Set itemKey programmatically after 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]").then($items => { + ($items.get(0) as any).itemKey = "sap_horizon"; + ($items.get(1) as any).itemKey = "sap_horizon_dark"; + }); + + cy.get("@appearanceView").then($view => { + $view.get(0).addEventListener("theme-selected", cy.stub().as("themeSelectedWithDetail")); + }); + + cy.get("@appearanceView") + .find("[ui5-user-settings-appearance-view-item]") + .eq(1) + .click(); + + cy.get("@themeSelectedWithDetail").should("have.been.calledOnce"); + cy.get("@themeSelectedWithDetail").then((stub: any) => { + const call = stub.getCall(0); + expect(call.args[0].detail.selectedTheme).to.equal("sap_horizon_dark"); + }); + }); + + it("tests item selection state", () => { + 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]").eq(1).then($item => { + const item = $item.get(0) as any; + expect(item.selected).to.be.true; + }); + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(0).then($item => { + const item = $item.get(0) as any; + expect(item.selected).to.be.false; + }); + }); + + it("tests item selection changes on click", () => { + 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]") + .first() + .click(); + + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(0).then($item => { + const item = $item.get(0) as any; + expect(item.selected).to.be.true; + }); + + cy.get("@appearanceView") + .find("[ui5-user-settings-appearance-view-item]") + .eq(1) + .click(); + + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(0).then($item => { + const item = $item.get(0) as any; + expect(item.selected).to.be.false; + }); + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(1).then($item => { + const item = $item.get(0) as any; + expect(item.selected).to.be.true; + }); + }); + + it("tests item clickable and hover states", () => { + 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("item"); + + cy.get("@item").then($item => { + const item = $item.get(0) as any; + expect(item.type).to.equal("Active"); + }); + + cy.get("@item").shadow().find(".ui5-li-root").should("exist"); + }); + + it("tests additionalContent slot", () => { + cy.mount( + + + +
Additional content here
+
+
+
); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + cy.get("@appearanceView").shadow().find("slot[name='additionalContent']").should("exist"); + }); + + it("tests additionalContent slot renders above items", () => { + cy.mount( + + + +
Additional content above items
+
+
+
); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + + // Get positions of additionalContent and the list + cy.get("@appearanceView").find("#additional-content-test").then($additional => { + const additionalTop = $additional.get(0).getBoundingClientRect().top; + + cy.get("@appearanceView").shadow().find("[ui5-list]").then($list => { + const listTop = $list.get(0).getBoundingClientRect().top; + + // Additional content should be positioned above the list + expect(additionalTop).to.be.lessThan(listTop); + }); + }); + }); + + it("tests selection persists across re-renders", () => { + cy.mount( + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + + cy.get("@appearanceView").then($view => { + const view = $view.get(0) as any; + expect(view.selectedItemKey).to.equal("sap_horizon"); + }); + + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(0).then($item => { + const item = $item.get(0) as any; + expect(item.selected).to.be.true; + }); + }); + + it("tests items in groups fire theme-selected event", () => { + cy.mount( + + + + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + + cy.get("@appearanceView").then($view => { + $view.get(0).addEventListener("theme-selected", cy.stub().as("themeSelected")); + }); + + cy.get("@appearanceView") + .find("[ui5-user-settings-appearance-view-group]") + .find("[ui5-user-settings-appearance-view-item]") + .first() + .click(); + + cy.get("@themeSelected").should("have.been.calledOnce"); + cy.get("@themeSelected").then((stub: any) => { + const call = stub.getCall(0); + expect(call.args[0].detail.selectedTheme).to.equal("sap_horizon"); + }); + }); + + it("tests appearance view list renders correctly", () => { + cy.mount( + + + + + + ); + cy.get("[ui5-user-settings-dialog]").as("settings"); + cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); + cy.get("@appearanceView").shadow().find("[ui5-list]").as("list"); + cy.get("@list").should("exist"); + cy.get("@list").should("have.class", "user-settings-appearance-view-list"); + }); +}); diff --git a/packages/fiori/src/UserSettingsAppearanceView.ts b/packages/fiori/src/UserSettingsAppearanceView.ts new file mode 100644 index 000000000000..318aa7e9b0aa --- /dev/null +++ b/packages/fiori/src/UserSettingsAppearanceView.ts @@ -0,0 +1,114 @@ +import UserSettingsView from "./UserSettingsView.js"; +import UserSettingsAppearanceViewTemplate from "./UserSettingsAppearanceViewTemplate.js"; +import UserSettingViewCss from "./generated/themes/UserSettingsView.css.js"; +import type UserSettingsAppearanceViewItem from "./UserSettingsAppearanceViewItem.js"; +import type UserSettingsAppearanceViewGroup from "./UserSettingsAppearanceViewGroup.js"; + +import { + customElement, slot, eventStrict as event, property, +} from "@ui5/webcomponents-base/dist/decorators.js"; +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; + +@customElement({ + tag: "ui5-user-settings-appearance-view", + renderer: jsxRenderer, + template: UserSettingsAppearanceViewTemplate, + styles: [UserSettingViewCss], +}) + +/** + * Fired when a theme is selected. + * @public + */ +@event("theme-selected") + +/** + * @class + * ### Overview + * + * The `ui5-user-settings-appearance-view` represents a view displayed in the `ui5-user-settings-item`. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceView.js";` + * + * @constructor + * @extends UserSettingsView + * @experimental + * @public + * @since 2.17.0 + */ +class UserSettingsAppearanceView extends UserSettingsView { + eventDetails!: { + "theme-selected": { selectedTheme: string }; + } + + /** + * Defines the currently selected theme key. + * @default "" + * @public + */ + @property() + selectedItemKey = ""; + + /** + * Defines the items of the component. + * + * @public + */ + @slot({ + type: HTMLElement, + "default": true, + invalidateOnChildChange: true, + }) + items!: Array; + + /** + * Defines additional content displayed below the items list. + * + * @public + */ + @slot({ + type: HTMLElement, + }) + additionalContent?: Array; + + onBeforeRendering() { + this._updateSelection(); + } + + _updateSelection() { + this._getAllItems().forEach(item => { + item.selected = item.itemKey === this.selectedItemKey; + }); + } + + _getAllItems(): Array { + const allItems: Array = []; + + this.items.forEach(item => { + if (item.tagName === "UI5-USER-SETTINGS-APPEARANCE-VIEW-GROUP") { + const group = item as UserSettingsAppearanceViewGroup; + const groupItems = Array.from(group.children).filter( + child => child.tagName === "UI5-USER-SETTINGS-APPEARANCE-VIEW-ITEM", + ) as Array; + allItems.push(...groupItems); + } else if (item.tagName === "UI5-USER-SETTINGS-APPEARANCE-VIEW-ITEM") { + allItems.push(item as UserSettingsAppearanceViewItem); + } + }); + + return allItems; + } + + _handleItemSelected = (e: CustomEvent) => { + const listItem = e.detail.item; + if (listItem.tagName === "UI5-USER-SETTINGS-APPEARANCE-VIEW-ITEM") { + const item = listItem as UserSettingsAppearanceViewItem; + this.selectedItemKey = item.itemKey; + this.fireDecoratorEvent("theme-selected", { selectedTheme: item.itemKey }); + } + }; +} + +UserSettingsAppearanceView.define(); +export default UserSettingsAppearanceView; diff --git a/packages/fiori/src/UserSettingsAppearanceViewGroup.ts b/packages/fiori/src/UserSettingsAppearanceViewGroup.ts new file mode 100644 index 000000000000..0c81047d4c45 --- /dev/null +++ b/packages/fiori/src/UserSettingsAppearanceViewGroup.ts @@ -0,0 +1,40 @@ +import ListItemGroup from "@ui5/webcomponents/dist/ListItemGroup.js"; +import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; +import slot from "@ui5/webcomponents-base/dist/decorators/slot.js"; +import type UserSettingsAppearanceViewItem from "./UserSettingsAppearanceViewItem.js"; + +/** + * @class + * ### Overview + * + * The `ui5-user-settings-appearance-view-group` is a special list item group used to group appearance view items. + * + * This is the item to use inside a `ui5-user-settings-appearance-view`. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceViewGroup.js";` + * + * @constructor + * @extends ListItemGroup + * @experimental + * @public + * @since 2.17.0 + */ +@customElement({ + tag: "ui5-user-settings-appearance-view-group", +}) +class UserSettingsAppearanceViewGroup extends ListItemGroup { + /** + * Defines the items of the ui5-user-settings-appearance-view-group. + * @public + */ + @slot({ + "default": true, + invalidateOnChildChange: true, + type: HTMLElement, + }) + declare items: Array; +} + +UserSettingsAppearanceViewGroup.define(); +export default UserSettingsAppearanceViewGroup; diff --git a/packages/fiori/src/UserSettingsAppearanceViewItem.ts b/packages/fiori/src/UserSettingsAppearanceViewItem.ts new file mode 100644 index 000000000000..d93968ac962b --- /dev/null +++ b/packages/fiori/src/UserSettingsAppearanceViewItem.ts @@ -0,0 +1,70 @@ +import UserSettingViewCss from "./generated/themes/UserSettingsView.css.js"; +import UserSettingsAppearanceViewItemTemplate from "./UserSettingsAppearanceViewItemTemplate.js"; +import UserSettingsAppearanceViewItemCss from "./generated/themes/UserSettingsAppearanceViewItem.css.js"; +import { + customElement, property, +} from "@ui5/webcomponents-base/dist/decorators.js"; +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import ListItemCustom from "@ui5/webcomponents/dist/ListItemCustom.js"; + +@customElement({ + tag: "ui5-user-settings-appearance-view-item", + renderer: jsxRenderer, + template: UserSettingsAppearanceViewItemTemplate, + styles: [ListItemCustom.styles, UserSettingViewCss, UserSettingsAppearanceViewItemCss], +}) + +/** + * @class + * ### Overview + * + * The `ui5-user-settings-appearance-view-item` represents a theme/appearance option item + * within the `ui5-user-settings-appearance-view`. + * + * It displays a theme with an avatar icon, text label, and can be selected. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceViewItem.js";` + * + * @constructor + * @extends ListItemCustom + * @experimental + * @public + * @since 2.17.0 + */ +class UserSettingsAppearanceViewItem extends ListItemCustom { + /** + * Defines the unique identifier of the item. + * @default "" + * @public + */ + @property() + itemKey = ""; + + /** + * Defines the text label displayed for the appearance item. + * @default "" + * @public + */ + @property() + text = ""; + + /** + * Defines the icon of the appearance item. + * @default "product" + * @public + */ + @property() + icon = "product"; + + /** + * Defines the color scheme of the avatar. + * @default "Accent7" + * @public + */ + @property() + colorScheme = "Accent7"; +} + +UserSettingsAppearanceViewItem.define(); +export default UserSettingsAppearanceViewItem; diff --git a/packages/fiori/src/UserSettingsAppearanceViewItemTemplate.tsx b/packages/fiori/src/UserSettingsAppearanceViewItemTemplate.tsx new file mode 100644 index 000000000000..b72242ceb14a --- /dev/null +++ b/packages/fiori/src/UserSettingsAppearanceViewItemTemplate.tsx @@ -0,0 +1,24 @@ +import type UserSettingsAppearanceViewItem from "./UserSettingsAppearanceViewItem.js"; +import ListItemCustomTemplate from "@ui5/webcomponents/dist/ListItemCustomTemplate.js"; +import Avatar from "@ui5/webcomponents/dist/Avatar.js"; +import AvatarSize from "@ui5/webcomponents/dist/types/AvatarSize.js"; + +export default function UserSettingsAppearanceViewItemTemplate(this: UserSettingsAppearanceViewItem) { + return ListItemCustomTemplate.call(this, { + listItemContent: listItemContent.bind(this), + }); +} + +function listItemContent(this: UserSettingsAppearanceViewItem) { + return ( +
+
+ + +
+ {this.text} +
+
+
+ ); +} diff --git a/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx b/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx new file mode 100644 index 000000000000..b8ef6e0077a8 --- /dev/null +++ b/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx @@ -0,0 +1,15 @@ +import type UserSettingsAppearanceView from "./UserSettingsAppearanceView.js"; +import List from "@ui5/webcomponents/dist/List.js"; + +export default function UserSettingsAppearanceViewTemplate(this: UserSettingsAppearanceView) { + return ( + + ); +} diff --git a/packages/fiori/src/bundle.esm.ts b/packages/fiori/src/bundle.esm.ts index 1d4c9bacc443..9ccfbb26b1af 100644 --- a/packages/fiori/src/bundle.esm.ts +++ b/packages/fiori/src/bundle.esm.ts @@ -51,6 +51,9 @@ import UserSettingsItem from "./UserSettingsItem.js"; import SettingsDialog from "./UserSettingsDialog.js"; import UserSettingsView from "./UserSettingsView.js"; import UserSettingsAccountView from "./UserSettingsAccountView.js"; +import UserSettingsAppearanceView from "./UserSettingsAppearanceView.js"; +import UserSettingsAppearanceViewItem from "./UserSettingsAppearanceViewItem.js"; +import UserSettingsAppearanceViewGroup from "./UserSettingsAppearanceViewGroup.js"; import Timeline from "./Timeline.js"; import TimelineGroupItem from "./TimelineGroupItem.js"; import NavigationLayout from "./NavigationLayout.js"; diff --git a/packages/fiori/src/themes/UserSettingsAppearanceViewItem.css b/packages/fiori/src/themes/UserSettingsAppearanceViewItem.css new file mode 100644 index 000000000000..ccb161523b74 --- /dev/null +++ b/packages/fiori/src/themes/UserSettingsAppearanceViewItem.css @@ -0,0 +1,65 @@ +.list-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.625rem 0; + width: 100%; +} + +.item-left { + display: flex; + align-items: center; + gap: 0.75rem; + flex: 1; +} + +/* Default: show cozy avatar, hide compact avatar */ +.avatar-cozy { + display: inline-block; +} + +.avatar-compact { + display: none; +} + +/* In compact mode: hide cozy avatar, show compact avatar */ +:host-context(.sapUiSizeCompact) .avatar-cozy, +:host-context(.ui5-content-density-compact) .avatar-cozy, +:host-context([data-ui5-compact-size]) .avatar-cozy { + display: none; +} + +:host-context(.sapUiSizeCompact) .avatar-compact, +:host-context(.ui5-content-density-compact) .avatar-compact, +:host-context([data-ui5-compact-size]) .avatar-compact { + display: inline-block; +} + +.item-texts { + display: flex; + flex-direction: column; +} + +.item-title { + font-weight: 600; + font-size: var(--sapFontSize); + color: var(--sapList_TextColor); + margin: 0.5rem; +} + +:host-context(.sapUiSizeCompact) .item-title, +:host-context([data-ui5-compact-size]) .item-title { + font-size: var(--sapFontLargeSize); +} + +.item-subtitle { + color: var(--sapContent_LabelColor); + font-size: var(--sapFontSize); + margin: 0.5rem; +} + +.item-right { + display: flex; + align-items: center; + gap: 0.5rem; +} diff --git a/packages/fiori/test/pages/UserSettingsDialog.html b/packages/fiori/test/pages/UserSettingsDialog.html index 07b21676955a..e0914ec06b3f 100644 --- a/packages/fiori/test/pages/UserSettingsDialog.html +++ b/packages/fiori/test/pages/UserSettingsDialog.html @@ -58,25 +58,24 @@ - - - SAP Morning Horizon - SAP Evening Horizon - SAP High Contrast Black (SAP Horizon) - SAP High Contrast White (SAP Horizon) - - Save - Changes applied. - - - - - - Increases the size and spacing of controls to allow you to interact with them more easily using your fingertip. - This is useful for hybrid devices that combine touch and mouse events. - - - + + + + + + + + + + + + + + + + + + @@ -410,7 +409,6 @@ const toast = [...document.getElementsByTagName("ui5-toast")][0]; const toastReset = document.getElementById("toastReset"); const toastResetAll = document.getElementById("toastResetAll"); - const themeSave =document.getElementById("themeSave"); const notificationsItem = document.getElementById("notifications"); const notificationNavIcon = document.querySelectorAll(".nav-icon"); const notificationSwitch = document.querySelectorAll(".switch"); @@ -464,10 +462,6 @@ mobileSecondPage.selected = true; mobileSecondPage.text = "Android"; }); - - themeSave.addEventListener("click", function () { - toast.open = true; - }); resetPersonalization.addEventListener("click", function () { toastReset.open = true; @@ -567,6 +561,17 @@ notificationSecondPage.text = title; }); }); + + // Theme switching functionality + const appearanceView = document.querySelector("ui5-user-settings-appearance-view"); + + appearanceView.addEventListener("theme-selected", (e) => { + const selectedKey = e.detail.selectedTheme; + + if (selectedKey) { + window["sap-ui-webcomponents-bundle"].configuration.setTheme(selectedKey); + } + }); \ No newline at end of file From 89a029ff5d305ddc3302028a162095aa90174ba4 Mon Sep 17 00:00:00 2001 From: Svilen Darvenyashki Date: Mon, 8 Dec 2025 14:26:57 +0200 Subject: [PATCH 2/6] chore(ui5-user-settings): resolve comments --- packages/fiori/README.md | 2 + .../cypress/specs/UserSettingsDialog.cy.tsx | 109 +++++++----------- .../fiori/src/UserSettingsAppearanceView.ts | 52 +++++---- ...UserSettingsAppearanceViewItemTemplate.tsx | 1 + .../UserSettingsAppearanceViewTemplate.tsx | 2 +- .../fiori/test/pages/UserSettingsDialog.html | 8 +- 6 files changed, 76 insertions(+), 98 deletions(-) diff --git a/packages/fiori/README.md b/packages/fiori/README.md index 6c02707f6e36..857a86d5ab42 100644 --- a/packages/fiori/README.md +++ b/packages/fiori/README.md @@ -41,6 +41,8 @@ such as a common header (ShellBar). | User Settings Item | `ui5-user-settings-item` | `import "@ui5/webcomponents-fiori/dist/UserSettingsItem.js";` | | User Settings Account View | `ui5-user-settings-account-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsAccountView.js";` | | User Settings Appearance View | `ui5-user-settings-appearance-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceView.js";`| +| User Settings Appearance View Item | `ui5-user-settings-appearance-view-item`| `import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceViewItem.js";`| +| User Settings Appearance View Group | `ui5-user-settings-appearance-view-group`| `import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceViewGroup.js";`| | User Settings View | `ui5-user-settings-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsView.js";` | | View Settings Dialog | `ui5-view-settings-dialog` | `import "@ui5/webcomponents-fiori/dist/ViewSettingsDialog.js";` | | View Settings Dialog - Sort Item | `ui5-sort-item` | `import "@ui5/webcomponents-fiori/dist/SortItem.js";` | diff --git a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx index 763c6d25f2cf..957debd089d4 100644 --- a/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx +++ b/packages/fiori/cypress/specs/UserSettingsDialog.cy.tsx @@ -1046,10 +1046,10 @@ describe("Appearance view", () => { cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").as("item"); cy.get("@item").shadow().find("[ui5-avatar]").as("avatar"); cy.get("@avatar").should("exist"); - cy.get("@avatar").should("have.length", 1); - cy.get("@avatar").should("have.attr", "icon", "palette"); - cy.get("@avatar").should("have.attr", "shape", "Square"); - cy.get("@avatar").should("have.attr", "color-scheme", "Accent7"); + cy.get("@avatar").should("have.length", 2); // Two avatars: one for cozy, one for compact mode + cy.get("@avatar").first().should("have.attr", "icon", "palette"); + cy.get("@avatar").first().should("have.attr", "shape", "Square"); + cy.get("@avatar").first().should("have.attr", "color-scheme", "Accent7"); }); it("tests appearance view item text displayed", () => { @@ -1123,7 +1123,7 @@ describe("Appearance view", () => { cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").should("have.length", 2); }); - it("tests theme-selected event", () => { + it("tests selection-change event", () => { cy.mount( @@ -1136,7 +1136,7 @@ describe("Appearance view", () => { cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); cy.get("@appearanceView").then($view => { - $view.get(0).addEventListener("theme-selected", cy.stub().as("themeSelected")); + $view.get(0).addEventListener("selection-change", cy.stub().as("selectionChange")); }); cy.get("@appearanceView") @@ -1144,29 +1144,24 @@ describe("Appearance view", () => { .first() .click(); - cy.get("@themeSelected").should("have.been.calledOnce"); + cy.get("@selectionChange").should("have.been.calledOnce"); }); - it("tests theme-selected event detail contains correct key", () => { + it("tests selection-change event detail contains correct item", () => { cy.mount( - - + + ); - // Set itemKey programmatically after 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]").then($items => { - ($items.get(0) as any).itemKey = "sap_horizon"; - ($items.get(1) as any).itemKey = "sap_horizon_dark"; - }); cy.get("@appearanceView").then($view => { - $view.get(0).addEventListener("theme-selected", cy.stub().as("themeSelectedWithDetail")); + $view.get(0).addEventListener("selection-change", cy.stub().as("selectionChangeWithDetail")); }); cy.get("@appearanceView") @@ -1174,69 +1169,33 @@ describe("Appearance view", () => { .eq(1) .click(); - cy.get("@themeSelectedWithDetail").should("have.been.calledOnce"); - cy.get("@themeSelectedWithDetail").then((stub: any) => { + cy.get("@selectionChangeWithDetail").should("have.been.calledOnce"); + cy.get("@selectionChangeWithDetail").then((stub: any) => { const call = stub.getCall(0); - expect(call.args[0].detail.selectedTheme).to.equal("sap_horizon_dark"); + expect(call.args[0].detail.item.itemKey).to.equal("sap_horizon_dark"); + expect(call.args[0].detail.item.text).to.equal("SAP Evening Horizon"); }); }); it("tests item selection state", () => { - 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]").eq(1).then($item => { - const item = $item.get(0) as any; - expect(item.selected).to.be.true; - }); - cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(0).then($item => { - const item = $item.get(0) as any; - expect(item.selected).to.be.false; - }); - }); - - it("tests item selection changes on click", () => { 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]") - .first() - .click(); - - cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(0).then($item => { + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(1).then($item => { const item = $item.get(0) as any; expect(item.selected).to.be.true; }); - - cy.get("@appearanceView") - .find("[ui5-user-settings-appearance-view-item]") - .eq(1) - .click(); - cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(0).then($item => { const item = $item.get(0) as any; expect(item.selected).to.be.false; }); - cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(1).then($item => { - const item = $item.get(0) as any; - expect(item.selected).to.be.true; - }); }); it("tests item clickable and hover states", () => { @@ -1298,11 +1257,11 @@ describe("Appearance view", () => { }); }); - it("tests selection persists across re-renders", () => { + it("tests selection-change event can be prevented", () => { cy.mount( - - + + @@ -1311,17 +1270,31 @@ describe("Appearance view", () => { cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); cy.get("@appearanceView").then($view => { - const view = $view.get(0) as any; - expect(view.selectedItemKey).to.equal("sap_horizon"); + $view.get(0).addEventListener("selection-change", (e: Event) => e.preventDefault()); + $view.get(0).addEventListener("selection-change", cy.stub().as("selectionChange")); }); + cy.get("@appearanceView") + .find("[ui5-user-settings-appearance-view-item]") + .eq(1) + .click(); + + cy.get("@selectionChange").should("have.been.calledOnce"); + + // First item should still be selected because event was prevented cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(0).then($item => { const item = $item.get(0) as any; expect(item.selected).to.be.true; }); + + // Second item should not be selected + cy.get("@appearanceView").find("[ui5-user-settings-appearance-view-item]").eq(1).then($item => { + const item = $item.get(0) as any; + expect(item.selected).to.be.false; + }); }); - it("tests items in groups fire theme-selected event", () => { + it("tests items in groups fire selection-change event", () => { cy.mount( @@ -1336,7 +1309,7 @@ describe("Appearance view", () => { cy.get("@settings").find("[ui5-user-settings-appearance-view]").as("appearanceView"); cy.get("@appearanceView").then($view => { - $view.get(0).addEventListener("theme-selected", cy.stub().as("themeSelected")); + $view.get(0).addEventListener("selection-change", cy.stub().as("selectionChange")); }); cy.get("@appearanceView") @@ -1345,10 +1318,10 @@ describe("Appearance view", () => { .first() .click(); - cy.get("@themeSelected").should("have.been.calledOnce"); - cy.get("@themeSelected").then((stub: any) => { + cy.get("@selectionChange").should("have.been.calledOnce"); + cy.get("@selectionChange").then((stub: any) => { const call = stub.getCall(0); - expect(call.args[0].detail.selectedTheme).to.equal("sap_horizon"); + expect(call.args[0].detail.item.itemKey).to.equal("sap_horizon"); }); }); diff --git a/packages/fiori/src/UserSettingsAppearanceView.ts b/packages/fiori/src/UserSettingsAppearanceView.ts index 318aa7e9b0aa..2a164a455697 100644 --- a/packages/fiori/src/UserSettingsAppearanceView.ts +++ b/packages/fiori/src/UserSettingsAppearanceView.ts @@ -3,12 +3,18 @@ import UserSettingsAppearanceViewTemplate from "./UserSettingsAppearanceViewTemp import UserSettingViewCss from "./generated/themes/UserSettingsView.css.js"; import type UserSettingsAppearanceViewItem from "./UserSettingsAppearanceViewItem.js"; import type UserSettingsAppearanceViewGroup from "./UserSettingsAppearanceViewGroup.js"; +import type { ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js"; +import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js"; import { customElement, slot, eventStrict as event, property, } from "@ui5/webcomponents-base/dist/decorators.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +type UserSettingsAppearanceViewItemSelectEventDetail = { + item: UserSettingsAppearanceViewItem; +} + @customElement({ tag: "ui5-user-settings-appearance-view", renderer: jsxRenderer, @@ -17,10 +23,13 @@ import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; }) /** - * Fired when a theme is selected. + * Fired when an item is selected. + * @param {UserSettingsAppearanceViewItem} item The selected `user settings appearance view item`. * @public */ -@event("theme-selected") +@event("selection-change", { + cancelable: true, +}) /** * @class @@ -39,17 +48,9 @@ import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; */ class UserSettingsAppearanceView extends UserSettingsView { eventDetails!: { - "theme-selected": { selectedTheme: string }; + "selection-change": UserSettingsAppearanceViewItemSelectEventDetail; } - /** - * Defines the currently selected theme key. - * @default "" - * @public - */ - @property() - selectedItemKey = ""; - /** * Defines the items of the component. * @@ -72,16 +73,6 @@ class UserSettingsAppearanceView extends UserSettingsView { }) additionalContent?: Array; - onBeforeRendering() { - this._updateSelection(); - } - - _updateSelection() { - this._getAllItems().forEach(item => { - item.selected = item.itemKey === this.selectedItemKey; - }); - } - _getAllItems(): Array { const allItems: Array = []; @@ -100,15 +91,26 @@ class UserSettingsAppearanceView extends UserSettingsView { return allItems; } - _handleItemSelected = (e: CustomEvent) => { - const listItem = e.detail.item; + _handleItemClick = (e: CustomEvent) => { + const listItem = e.detail.item as ListItemBase & { associatedSettingItem?: UserSettingsAppearanceViewItem }; if (listItem.tagName === "UI5-USER-SETTINGS-APPEARANCE-VIEW-ITEM") { const item = listItem as UserSettingsAppearanceViewItem; - this.selectedItemKey = item.itemKey; - this.fireDecoratorEvent("theme-selected", { selectedTheme: item.itemKey }); + const eventPrevented = !this.fireDecoratorEvent("selection-change", { + item, + }); + + if (!eventPrevented) { + this._getAllItems().forEach(viewItem => { + viewItem.selected = false; + }); + item.selected = true; + } } }; } UserSettingsAppearanceView.define(); export default UserSettingsAppearanceView; +export type { + UserSettingsAppearanceViewItemSelectEventDetail, +}; diff --git a/packages/fiori/src/UserSettingsAppearanceViewItemTemplate.tsx b/packages/fiori/src/UserSettingsAppearanceViewItemTemplate.tsx index b72242ceb14a..8c471d0ca926 100644 --- a/packages/fiori/src/UserSettingsAppearanceViewItemTemplate.tsx +++ b/packages/fiori/src/UserSettingsAppearanceViewItemTemplate.tsx @@ -13,6 +13,7 @@ function listItemContent(this: UserSettingsAppearanceViewItem) { return (
+ {/* Two avatars are rendered for different content density modes - CSS controls visibility */}
diff --git a/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx b/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx index b8ef6e0077a8..3a1ae912ee0a 100644 --- a/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx +++ b/packages/fiori/src/UserSettingsAppearanceViewTemplate.tsx @@ -6,7 +6,7 @@ export default function UserSettingsAppearanceViewTemplate(this: UserSettingsApp