From 1571cbc081233abf84fdfe8f36a73c35d771ca65 Mon Sep 17 00:00:00 2001 From: Dobromira Boycheva Date: Mon, 3 Aug 2026 11:34:03 +0300 Subject: [PATCH 1/7] feat(ui5-user-settings-notifications-view): introduce NotificationsView component family --- packages/fiori/README.md | 3 + .../UserSettingsNotificationsView.cy.tsx | 813 ++++++++++++++++++ packages/fiori/src/UserSettingsItem.ts | 8 + .../src/UserSettingsNotificationsView.ts | 186 ++++ .../src/UserSettingsNotificationsViewGroup.ts | 49 ++ .../src/UserSettingsNotificationsViewItem.ts | 157 ++++ ...rSettingsNotificationsViewItemTemplate.tsx | 50 ++ .../UserSettingsNotificationsViewTemplate.tsx | 19 + packages/fiori/src/bundle.esm.ts | 3 + .../UserSettingsNotificationsViewGroup.css | 13 + .../UserSettingsNotificationsViewItem.css | 98 +++ .../fiori/test/pages/UserSettingsDialog.html | 478 ++++------ .../fiori/UserSettingsDialog/Basic/main.js | 3 + .../UserSettingsDialog/Basic/sample.html | 30 +- .../fiori/UserSettingsDialog/Basic/sample.tsx | 74 +- .../patterns/UXCIntegration/Basic/main.js | 3 + .../patterns/UXCIntegration/Basic/sample.html | 19 +- 17 files changed, 1690 insertions(+), 316 deletions(-) create mode 100644 packages/fiori/cypress/specs/UserSettingsNotificationsView.cy.tsx create mode 100644 packages/fiori/src/UserSettingsNotificationsView.ts create mode 100644 packages/fiori/src/UserSettingsNotificationsViewGroup.ts create mode 100644 packages/fiori/src/UserSettingsNotificationsViewItem.ts create mode 100644 packages/fiori/src/UserSettingsNotificationsViewItemTemplate.tsx create mode 100644 packages/fiori/src/UserSettingsNotificationsViewTemplate.tsx create mode 100644 packages/fiori/src/themes/UserSettingsNotificationsViewGroup.css create mode 100644 packages/fiori/src/themes/UserSettingsNotificationsViewItem.css diff --git a/packages/fiori/README.md b/packages/fiori/README.md index 1a810a9a8a10f..3447e98350a1b 100644 --- a/packages/fiori/README.md +++ b/packages/fiori/README.md @@ -44,6 +44,9 @@ such as a common header (ShellBar). | 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";` | +| User Settings Notifications View | `ui5-user-settings-notifications-view` | `import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsView.js";` | +| User Settings Notifications View Group | `ui5-user-settings-notifications-view-group` | `import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsViewGroup.js";` | +| User Settings Notifications View Item | `ui5-user-settings-notifications-view-item` | `import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsViewItem.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";` | diff --git a/packages/fiori/cypress/specs/UserSettingsNotificationsView.cy.tsx b/packages/fiori/cypress/specs/UserSettingsNotificationsView.cy.tsx new file mode 100644 index 0000000000000..82d4b9d227827 --- /dev/null +++ b/packages/fiori/cypress/specs/UserSettingsNotificationsView.cy.tsx @@ -0,0 +1,813 @@ +import UserSettingsDialog from "../../src/UserSettingsDialog.js"; +import UserSettingsItem from "../../src/UserSettingsItem.js"; +import UserSettingsView from "../../src/UserSettingsView.js"; +import UserSettingsNotificationsView from "../../src/UserSettingsNotificationsView.js"; +import UserSettingsNotificationsViewGroup, { isInstanceOfUserSettingsNotificationsViewGroup } from "../../src/UserSettingsNotificationsViewGroup.js"; +import UserSettingsNotificationsViewItem, { isInstanceOfUserSettingsNotificationsViewItem } from "../../src/UserSettingsNotificationsViewItem.js"; +import MessageStrip from "@ui5/webcomponents/dist/MessageStrip.js"; +import Select from "@ui5/webcomponents/dist/Select.js"; +import Option from "@ui5/webcomponents/dist/Option.js"; + +describe("Notifications view", () => { + it("renders the view with no items", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").should("exist"); + cy.get("[ui5-user-settings-notifications-view]").shadow().find("[ui5-list]").should("exist"); + }); + + it("renders items with title, byline and switch", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").as("item"); + cy.get("@item").should("have.attr", "text", "SuccessFactors"); + cy.get("@item").should("have.attr", "byline-text", "Compensation | Learning | HR"); + cy.get("@item").should("have.attr", "checked"); + cy.get("@item").should("have.attr", "navigable"); + + cy.get("@item").shadow().find(".ui5-user-settings-notifications-item-title").contains("SuccessFactors"); + cy.get("@item").shadow().find(".ui5-user-settings-notifications-item-byline").contains("Compensation | Learning | HR"); + cy.get("@item").shadow().find("[ui5-switch]").should("exist"); + cy.get("@item").shadow().find("[ui5-switch]").should("have.attr", "checked"); + cy.get("@item").shadow().find(".ui5-user-settings-notifications-item-arrow").should("exist"); + }); + + it("renders items without arrow when not navigable", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").as("item"); + cy.get("@item").shadow().find(".ui5-user-settings-notifications-item-arrow").should("not.exist"); + }); + + it("fires switch-change event when the switch is toggled", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").as("item"); + cy.get("@item").then($item => { + $item.get(0).addEventListener("switch-change", cy.stub().as("switchChanged")); + }); + + cy.get("@item").shadow().find("[ui5-switch]").click(); + cy.get("@switchChanged").should("have.been.calledOnce"); + }); + + it("fires item-click on the view only for navigable items", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").as("view"); + cy.get("@view").then($view => { + $view.get(0).addEventListener("item-click", cy.stub().as("itemClick")); + }); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("@itemClick").should("have.been.calledOnce"); + cy.get("@itemClick").then((stub: any) => { + const call = stub.getCall(0); + expect(call.args[0].detail.item.text).to.equal("Navigable"); + }); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(1).click(); + cy.get("@itemClick").should("have.been.calledOnce"); + }); + + it("renders groups with items", () => { + cy.mount( + + + + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-group]").should("have.length", 2); + cy.get("[ui5-user-settings-notifications-view-item]").should("have.length", 3); + }); + + it("renders additionalContent slot above the list", () => { + cy.mount( + + + + Some settings are managed by your organization. + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").as("view"); + cy.get("@view").find("#ns-info").should("exist"); + }); + + it("renders endContent slot instead of the switch and does not fire switch-change", () => { + cy.mount( + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").as("item"); + cy.get("@item").shadow().find("[ui5-switch]").should("not.exist"); + cy.get("@item").find("#freq-select").should("exist"); + + cy.get("[ui5-user-settings-notifications-view]").as("view"); + cy.get("@item").then($item => { + $item.get(0).addEventListener("switch-change", cy.stub().as("itemSwitchChange")); + }); + cy.get("@view").then($view => { + $view.get(0).addEventListener("switch-change", cy.stub().as("viewSwitchChange")); + }); + + cy.get("#freq-select").click(); + cy.get("#freq-select").find("[ui5-option]").eq(1).click(); + + cy.get("@itemSwitchChange").should("not.have.been.called"); + cy.get("@viewSwitchChange").should("not.have.been.called"); + }); + + it("does not navigate to the secondary view when item-click is prevented", () => { + cy.mount( + + + + + + secondary content + + + ); + + cy.get("[ui5-user-settings-notifications-view]").as("view"); + cy.get("@view").then($view => { + $view.get(0).addEventListener("item-click", (e: Event) => { + e.preventDefault(); + }); + $view.get(0).addEventListener("item-click", cy.stub().as("itemClick")); + }); + + cy.get("[ui5-user-settings-notifications-view-item]").click(); + + cy.get("@itemClick").should("have.been.calledOnce"); + cy.get("#details-page").should("not.have.attr", "selected"); + }); + + it("getItemByKey returns the matching item", () => { + cy.mount( + + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").then($view => { + const view = $view.get(0) as UserSettingsNotificationsView; + const item = view.getItemByKey("sales-approvals"); + expect(item).to.exist; + expect(item!.text).to.equal("Sales Order Approvals"); + + const rootItem = view.getItemByKey("allow"); + expect(rootItem).to.exist; + expect(rootItem!.text).to.equal("Allow Notifications"); + + expect(view.getItemByKey("missing")).to.be.undefined; + }); + }); + + it("getAllItems returns a flat list including grouped items", () => { + cy.mount( + + + + + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").then($view => { + const view = $view.get(0) as UserSettingsNotificationsView; + const all = view.getAllItems(); + expect(all).to.have.length(4); + expect(all.map(i => i.text)).to.deep.equal([ + "Allow Notifications", + "Sales Order Updates", + "Sales Order Approvals", + "Purchase Order Approval", + ]); + }); + }); + + it("routes to the secondary view whose id matches the clicked item's itemKey", () => { + cy.mount( + + + + + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("#one").should("have.attr", "selected"); + cy.get("#one").should("have.attr", "text", "One detail page"); + cy.get("#two").should("not.have.attr", "selected"); + }); + + it("falls back to the first secondary view when no id matches", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("#fallback").should("have.attr", "selected"); + cy.get("#fallback").should("have.attr", "text", "Any"); + }); + + it("does not move focus on mouse drill-in (no lingering ring for mouse users)", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("#detail").should("have.attr", "selected"); + // Mouse-triggered drill-in should NOT auto-focus the back button. + cy.get("[ui5-user-settings-item]").shadow() + .find(".ui5-user-settings-item-collapse-btn") + .should("not.be.focused"); + }); +}); + +describe("Notifications view item", () => { + it("reflects the itemKey attribute", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").should("have.attr", "item-key", "allow"); + }); + + it("does not render the title span when text is empty", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find(".ui5-user-settings-notifications-item-title").should("not.exist"); + }); + + it("does not set the has-byline class when bylineText is empty", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find(".ui5-user-settings-notifications-item") + .should("not.have.class", "has-byline"); + }); + + it("sets the has-byline class when bylineText is present", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find(".ui5-user-settings-notifications-item.has-byline").should("exist"); + }); + + it("reflects the initial checked state on the internal switch", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find("[ui5-switch]").should("have.attr", "checked"); + + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find("[ui5-switch]").should("not.have.attr", "checked"); + }); + + it("sets the accessible name of the switch to the text when no byline", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find("[ui5-switch]").should("have.attr", "accessible-name", "Foo"); + }); + + it("sets the accessible name of the switch to text and byline when byline is present", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find("[ui5-switch]").should("have.attr", "accessible-name", "Foo Bar"); + }); + + it("fires switch-change with the item and checked flag in detail", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").as("item"); + cy.get("@item").then($item => { + $item.get(0).addEventListener("switch-change", cy.stub().as("switchChanged")); + }); + + cy.get("@item").shadow().find("[ui5-switch]").click(); + cy.get("@switchChanged").should("have.been.calledOnce"); + cy.get("@item").then($item => { + cy.get("@switchChanged").then((stub: any) => { + const call = stub.getCall(0); + expect(call.args[0].detail.item).to.equal($item.get(0)); + expect(call.args[0].detail.checked).to.be.a("boolean"); + }); + }); + }); + + it("bubbles switch-change to a wrapping parent element", () => { + cy.mount( + + +
+ +
+
+
+
); + + cy.get("#wrapper").then($wrapper => { + $wrapper.get(0).addEventListener("switch-change", cy.stub().as("outerSwitchChanged")); + }); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow().find("[ui5-switch]").click(); + cy.get("@outerSwitchChanged").should("have.been.calledOnce"); + }); + + it("isInstanceOfUserSettingsNotificationsViewItem identifies items correctly", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").then($item => { + expect(isInstanceOfUserSettingsNotificationsViewItem($item.get(0))).to.be.true; + const div = document.createElement("div"); + expect(isInstanceOfUserSettingsNotificationsViewItem(div)).to.be.false; + }); + }); + + it("renders switch when endContent is empty and hides it when endContent is provided", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find("[ui5-switch]").should("exist"); + + cy.mount( + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find("[ui5-switch]").should("not.exist"); + cy.get("[ui5-user-settings-notifications-view-item]").find("#freq-select").should("exist"); + }); + + it("renders a slim-arrow-right icon when navigable", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find("[ui5-icon][name=\"slim-arrow-right\"]").should("exist"); + }); + + it("updates the title when text is changed programmatically", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").then($item => { + const item = $item.get(0) as UserSettingsNotificationsViewItem; + item.text = "Updated"; + }); + + cy.get("[ui5-user-settings-notifications-view-item]").shadow() + .find(".ui5-user-settings-notifications-item-title").should("contain.text", "Updated"); + }); +}); + +describe("Notifications view — event forwarding", () => { + it("re-fires switch-change at the view level with {item, checked}", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").as("view"); + cy.get("[ui5-user-settings-notifications-view-item]").as("item"); + + cy.get("@view").then($view => { + $view.get(0).addEventListener("switch-change", cy.stub().as("viewSwitchChange")); + }); + + cy.get("@item").shadow().find("[ui5-switch]").click(); + + cy.get("@viewSwitchChange").should("have.been.calledOnce"); + cy.get("@viewSwitchChange").then((stub: any) => { + const call = stub.getCall(0); + cy.get("@item").then($item => { + expect(call.args[0].detail.item).to.equal($item.get(0)); + }); + expect(call.args[0].detail.checked).to.equal(true); + }); + }); + + it("does not fire item-click on the view for non-navigable items", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").as("view"); + cy.get("@view").then($view => { + $view.get(0).addEventListener("item-click", cy.stub().as("itemClick")); + }); + + cy.get("[ui5-user-settings-notifications-view-item]").click(); + + cy.get("@itemClick").should("not.have.been.called"); + }); + + it("fires item-click only for the navigable clicked item", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").as("view"); + cy.get("@view").then($view => { + $view.get(0).addEventListener("item-click", cy.stub().as("itemClick")); + }); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("[ui5-user-settings-notifications-view-item]").eq(1).click(); + + cy.get("@itemClick").should("have.been.calledOnce"); + cy.get("@itemClick").then((stub: any) => { + const call = stub.getCall(0); + expect(call.args[0].detail.item.text).to.equal("Navigable"); + }); + }); + + it("item-click event is cancelable", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").as("view"); + cy.get("@view").then($view => { + $view.get(0).addEventListener("item-click", cy.stub().as("itemClick")); + }); + + cy.get("[ui5-user-settings-notifications-view-item]").click(); + + cy.get("@itemClick").should("have.been.calledOnce"); + cy.get("@itemClick").then((stub: any) => { + const call = stub.getCall(0); + expect(call.args[0].cancelable).to.be.true; + }); + }); +}); + +describe("Notifications view group", () => { + it("renders items inside a group", () => { + cy.mount( + + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-group]").as("group"); + cy.get("@group").should("have.attr", "header-text", "Sales"); + cy.get("@group").find("[ui5-user-settings-notifications-view-item]").should("have.length", 3); + }); + + it("isInstanceOfUserSettingsNotificationsViewGroup returns true for the group and false for a plain div", () => { + cy.mount( + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-group]").then($group => { + expect(isInstanceOfUserSettingsNotificationsViewGroup($group.get(0))).to.equal(true); + const div = document.createElement("div"); + expect(isInstanceOfUserSettingsNotificationsViewGroup(div)).to.equal(false); + }); + }); + + it("getAllItems on the view includes items from all groups plus non-grouped items", () => { + cy.mount( + + + + + + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").then($view => { + const view = $view.get(0) as UserSettingsNotificationsView; + expect(view.getAllItems().length).to.equal(5); + }); + }); + + it("reflects the headerText attribute", () => { + cy.mount( + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-group]").should("have.attr", "header-text", "Sales"); + }); +}); + +describe("Notifications view — drill-in edge cases", () => { + it("keeps the target view's own text when the target was matched by itemKey", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("#specific").should("have.attr", "selected"); + cy.get("#specific").should("have.attr", "text", "Preserved title"); + }); + + it("uses the clicked item's text when the target was the fallback view — and updates on subsequent clicks", () => { + cy.mount( + + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("#fallback").should("have.attr", "text", "First"); + + // Simulate back navigation before triggering the second drill-in. + cy.get("#fallback").invoke("removeAttr", "selected"); + cy.get("#primary").invoke("attr", "selected", ""); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(1).click(); + cy.get("#fallback").should("have.attr", "text", "Second"); + }); + + it("does not drill in when the switch of a navigable item is clicked", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).shadow() + .find("[ui5-switch]").click(); + cy.get("#drill").should("not.have.attr", "selected"); + }); + + it("deselects the primary view when drilling into a secondary view", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("#target").should("have.attr", "selected"); + cy.get("#primary").should("not.have.attr", "selected"); + }); + + it("does not drill in when clicking inside the endContent slot on a navigable item", () => { + cy.mount( + + + + + + + + + + + ); + + cy.get("#end-select").click(); + cy.get("#drill").should("not.have.attr", "selected"); + }); +}); + diff --git a/packages/fiori/src/UserSettingsItem.ts b/packages/fiori/src/UserSettingsItem.ts index e2dbcbb990285..5cc402b199510 100644 --- a/packages/fiori/src/UserSettingsItem.ts +++ b/packages/fiori/src/UserSettingsItem.ts @@ -251,6 +251,14 @@ class UserSettingsItem extends UI5Element { return !!(this._hasSelectedPageView && this._selectedPageView.secondary); } + /** + * @private + */ + _focusBackButton() { + const backButton = this.shadowRoot?.querySelector(".ui5-user-settings-item-collapse-btn"); + backButton?.focus(); + } + captureRef(this: UserSettingsView, ref: HTMLElement & { associatedSettingView?: UserSettingsView} | null) { if (ref) { ref.associatedSettingView = this; diff --git a/packages/fiori/src/UserSettingsNotificationsView.ts b/packages/fiori/src/UserSettingsNotificationsView.ts new file mode 100644 index 0000000000000..82cff1544c717 --- /dev/null +++ b/packages/fiori/src/UserSettingsNotificationsView.ts @@ -0,0 +1,186 @@ +import UserSettingsView from "./UserSettingsView.js"; +import UserSettingsNotificationsViewTemplate from "./UserSettingsNotificationsViewTemplate.js"; +import UserSettingViewCss from "./generated/themes/UserSettingsView.css.js"; +import type UserSettingsNotificationsViewItem from "./UserSettingsNotificationsViewItem.js"; +import { isInstanceOfUserSettingsNotificationsViewItem } from "./UserSettingsNotificationsViewItem.js"; +import type UserSettingsNotificationsViewGroup from "./UserSettingsNotificationsViewGroup.js"; +import { isInstanceOfUserSettingsNotificationsViewGroup } from "./UserSettingsNotificationsViewGroup.js"; +import type UserSettingsItem from "./UserSettingsItem.js"; +import type { ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js"; +import type ListItemBase from "@ui5/webcomponents/dist/ListItemBase.js"; +import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js"; + +import { + customElement, slotStrict as slot, eventStrict as event, +} from "@ui5/webcomponents-base/dist/decorators.js"; +import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; +import type { DefaultSlot, Slot } from "@ui5/webcomponents-base/dist/UI5Element.js"; + +type UserSettingsNotificationsViewItemClickEventDetail = { + item: UserSettingsNotificationsViewItem; +} + +/** + * @class + * ### Overview + * + * The `ui5-user-settings-notifications-view` represents a view displayed in the + * `ui5-user-settings-item` that lists notification preferences. Individual settings + * are represented by `ui5-user-settings-notifications-view-item` elements, optionally + * grouped by `ui5-user-settings-notifications-view-group`. + * + * When a navigable item is clicked, the view drills into a sibling secondary view of + * its parent `ui5-user-settings-item`. When an item's `item-key` matches a target + * view's `id`, that view is opened and keeps its own `text`. Otherwise the first + * sibling marked as `secondary` is opened and its `text` is set to the clicked + * item's `text` so the drill-in header reflects the origin. + * + * Apps can override this behavior by preventing the `item-click` event. + * + * Applications should listen to the item's `switch-change` event (which bubbles) to + * be notified when a switch is toggled. + * + * Additional content (e.g. an information message strip) can be placed via the + * `additionalContent` slot, which is rendered above the list. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsView.js";` + * + * @constructor + * @extends UserSettingsView + * @public + * @since 2.25.0 + */ +@customElement({ + tag: "ui5-user-settings-notifications-view", + renderer: jsxRenderer, + template: UserSettingsNotificationsViewTemplate, + styles: [UserSettingViewCss], +}) + +/** + * Fired when a navigable item in the list is clicked. + * + * The event is cancelable: preventing it skips the built-in drill-in to the + * parent's secondary view, allowing the application to take over. + * + * @param {UserSettingsNotificationsViewItem} item The clicked notifications view item. + * @public + */ +@event("item-click", { + cancelable: true, +}) + +class UserSettingsNotificationsView extends UserSettingsView { + eventDetails!: { + "item-click": UserSettingsNotificationsViewItemClickEventDetail; + } + + /** + * Defines the items of the component. Can be a mix of + * `ui5-user-settings-notifications-view-item` and + * `ui5-user-settings-notifications-view-group` elements. + * + * @public + */ + @slot({ + type: HTMLElement, + "default": true, + invalidateOnChildChange: true, + }) + items!: DefaultSlot; + + /** + * Defines additional content displayed above the items list. + * + * @public + */ + @slot({ + type: HTMLElement, + }) + additionalContent!: Slot; + + /** + * Returns a flat list of all notification items, including items nested inside groups. + * + * @public + */ + getAllItems(): Array { + const allItems: Array = []; + + this.items.forEach(item => { + if (isInstanceOfUserSettingsNotificationsViewGroup(item)) { + item.items.forEach(child => { + if (isInstanceOfUserSettingsNotificationsViewItem(child)) { + allItems.push(child); + } + }); + } else if (isInstanceOfUserSettingsNotificationsViewItem(item)) { + allItems.push(item); + } + }); + + return allItems; + } + + /** + * Returns the first item with the given `itemKey`, or `undefined` if none matches. + * + * @public + */ + getItemByKey(itemKey: string): UserSettingsNotificationsViewItem | undefined { + return this.getAllItems().find(item => item.itemKey === itemKey); + } + + _navigateToSecondaryView(item: UserSettingsNotificationsViewItem) { + const parentItem = this.closest("[ui5-user-settings-item]"); + const secondaryViews = parentItem?.pages?.filter(view => view !== this && view.secondary) ?? []; + + const matched = item.itemKey ? secondaryViews.find(view => view.id === item.itemKey) : undefined; + const target = matched ?? secondaryViews[0]; + + if (!target) { + return; + } + + if (!matched) { + target.text = item.text; + } + + this.selected = false; + target.selected = true; + + if (item.matches(":focus-visible")) { + renderFinished().then(() => { + if (!this.isConnected) { + return; + } + parentItem?._focusBackButton?.(); + }); + } + } + + _handleItemClick = (e: CustomEvent) => { + const listItem = e.detail.item as ListItemBase; + if (!isInstanceOfUserSettingsNotificationsViewItem(listItem) || !listItem.navigable) { + return; + } + + const eventPrevented = !this.fireDecoratorEvent("item-click", { + item: listItem, + }); + + if (eventPrevented) { + e.preventDefault(); + return; + } + + this._navigateToSecondaryView(listItem); + }; +} + +UserSettingsNotificationsView.define(); +export default UserSettingsNotificationsView; +export type { + UserSettingsNotificationsViewItemClickEventDetail, +}; diff --git a/packages/fiori/src/UserSettingsNotificationsViewGroup.ts b/packages/fiori/src/UserSettingsNotificationsViewGroup.ts new file mode 100644 index 0000000000000..1c1063bc5f0d9 --- /dev/null +++ b/packages/fiori/src/UserSettingsNotificationsViewGroup.ts @@ -0,0 +1,49 @@ +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-strict.js"; +import createInstanceChecker from "@ui5/webcomponents-base/dist/util/createInstanceChecker.js"; +import UserSettingsNotificationsViewGroupCss from "./generated/themes/UserSettingsNotificationsViewGroup.css.js"; +import type UserSettingsNotificationsViewItem from "./UserSettingsNotificationsViewItem.js"; +import type { DefaultSlot } from "@ui5/webcomponents-base/dist/UI5Element.js"; + +/** + * @class + * ### Overview + * + * The `ui5-user-settings-notifications-view-group` groups `ui5-user-settings-notifications-view-item` + * elements inside a `ui5-user-settings-notifications-view`. Its header renders as a plain bold + * section title with a separator line below, per the notifications design spec. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsViewGroup.js";` + * + * @constructor + * @extends ListItemGroup + * @public + * @since 2.25.0 + */ +@customElement({ + tag: "ui5-user-settings-notifications-view-group", + styles: [ListItemGroup.styles, UserSettingsNotificationsViewGroupCss], +}) +class UserSettingsNotificationsViewGroup extends ListItemGroup { + /** + * Defines the items of the ui5-user-settings-notifications-view-group. + * @public + */ + @slot({ + "default": true, + invalidateOnChildChange: true, + type: HTMLElement, + }) + declare items: DefaultSlot; + + get isUserSettingsNotificationsViewGroup(): boolean { + return true; + } +} + +UserSettingsNotificationsViewGroup.define(); + +export const isInstanceOfUserSettingsNotificationsViewGroup = createInstanceChecker("isUserSettingsNotificationsViewGroup"); +export default UserSettingsNotificationsViewGroup; diff --git a/packages/fiori/src/UserSettingsNotificationsViewItem.ts b/packages/fiori/src/UserSettingsNotificationsViewItem.ts new file mode 100644 index 0000000000000..297e15bbdc429 --- /dev/null +++ b/packages/fiori/src/UserSettingsNotificationsViewItem.ts @@ -0,0 +1,157 @@ +import UserSettingsNotificationsViewItemTemplate from "./UserSettingsNotificationsViewItemTemplate.js"; +import UserSettingsNotificationsViewItemCss from "./generated/themes/UserSettingsNotificationsViewItem.css.js"; +import UserSettingViewCss from "./generated/themes/UserSettingsView.css.js"; +import { + customElement, property, slotStrict as slot, eventStrict as event, +} 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"; +import createInstanceChecker from "@ui5/webcomponents-base/dist/util/createInstanceChecker.js"; +import type Switch from "@ui5/webcomponents/dist/Switch.js"; +import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js"; + +type UserSettingsNotificationsViewItemSwitchChangeEventDetail = { + item: UserSettingsNotificationsViewItem; + checked: boolean; +} + +/** + * @class + * ### Overview + * + * The `ui5-user-settings-notifications-view-item` represents a single notification setting + * within the `ui5-user-settings-notifications-view`. + * + * It displays a title and an optional byline. By default a trailing switch reflects + * the `checked` state. Applications can override the trailing control by providing content + * in the `endContent` slot (e.g. a `ui5-select` for a value picker); the built-in switch and + * its `switch-change` event are then suppressed. Items can additionally be flagged as + * `navigable` to display a navigation arrow and behave as clickable list rows. + * + * **Note:** The default switch and the `endContent` slot are mutually exclusive. + * When any content is provided in `endContent`, the trailing switch is not rendered + * and no `switch-change` event is fired. + * + * ### ES6 Module Import + * `import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsViewItem.js";` + * + * @constructor + * @extends ListItemCustom + * @public + * @since 2.25.0 + */ +@customElement({ + tag: "ui5-user-settings-notifications-view-item", + renderer: jsxRenderer, + template: UserSettingsNotificationsViewItemTemplate, + styles: [ListItemCustom.styles, UserSettingViewCss, UserSettingsNotificationsViewItemCss], +}) + +/** + * Fired when the switch state changes. + * + * Not fired when the `endContent` slot is used to override the trailing control. + * + * @param {UserSettingsNotificationsViewItem} item The item whose switch was toggled. + * @param {boolean} checked The new checked state of the switch. + * @public + */ +@event("switch-change", { + bubbles: true, +}) + +class UserSettingsNotificationsViewItem extends ListItemCustom { + eventDetails!: ListItemCustom["eventDetails"] & { + "switch-change": UserSettingsNotificationsViewItemSwitchChangeEventDetail; + } + + /** + * Defines the unique identifier of the item. + * + * When the item is navigable, `itemKey` is also used to route to a matching sibling + * `secondary` view by id. + * + * @default "" + * @public + */ + @property() + itemKey = ""; + + /** + * Defines the title text of the item. + * @default "" + * @public + */ + @property() + text = ""; + + /** + * Defines the byline text of the item, rendered below the title. + * @default "" + * @public + */ + @property() + bylineText = ""; + + /** + * Defines whether the trailing switch is on. + * + * Ignored when the `endContent` slot is used. + * + * @default false + * @public + */ + @property({ type: Boolean }) + checked = false; + + /** + * Defines whether the item is navigable. When true, a navigation arrow is rendered + * and the whole row becomes clickable (fires the parent view's `item-click` event). + * @default false + * @public + */ + @property({ type: Boolean }) + navigable = false; + + /** + * Defines custom content rendered at the trailing end of the item, replacing the + * default switch. Use this to place a `ui5-select`, `ui5-input`, or any other + * control instead of a boolean toggle. + * + * @public + */ + @slot({ + type: HTMLElement, + }) + endContent!: Slot; + + get isUserSettingsNotificationsViewItem(): boolean { + return true; + } + + get _hasEndContent(): boolean { + return this.endContent.length > 0; + } + + get _accessibleSwitchName(): string { + return this.bylineText ? `${this.text} ${this.bylineText}` : this.text; + } + + _handleSwitchChange = (e: Event) => { + const target = e.target as Switch; + this.checked = target.checked; + this.fireDecoratorEvent("switch-change", { item: this, checked: this.checked }); + }; + + _handleEndClick = (e: MouseEvent) => { + e.stopPropagation(); + }; +} + +UserSettingsNotificationsViewItem.define(); + +export const isInstanceOfUserSettingsNotificationsViewItem = createInstanceChecker("isUserSettingsNotificationsViewItem"); +export default UserSettingsNotificationsViewItem; +export type { + UserSettingsNotificationsViewItemSwitchChangeEventDetail, +}; diff --git a/packages/fiori/src/UserSettingsNotificationsViewItemTemplate.tsx b/packages/fiori/src/UserSettingsNotificationsViewItemTemplate.tsx new file mode 100644 index 0000000000000..c550719c5e9f9 --- /dev/null +++ b/packages/fiori/src/UserSettingsNotificationsViewItemTemplate.tsx @@ -0,0 +1,50 @@ +import type UserSettingsNotificationsViewItem from "./UserSettingsNotificationsViewItem.js"; +import ListItemCustomTemplate from "@ui5/webcomponents/dist/ListItemCustomTemplate.js"; +import Switch from "@ui5/webcomponents/dist/Switch.js"; +import Icon from "@ui5/webcomponents/dist/Icon.js"; +import slimArrowRight from "@ui5/webcomponents-icons/dist/slim-arrow-right.js"; + +export default function UserSettingsNotificationsViewItemTemplate(this: UserSettingsNotificationsViewItem) { + return ListItemCustomTemplate.call(this, { + listItemContent: listItemContent.bind(this), + }); +} + +function listItemContent(this: UserSettingsNotificationsViewItem) { + const rootClasses = ["ui5-user-settings-notifications-item"]; + if (this.bylineText && this.text) { + rootClasses.push("has-byline"); + } + + return ( +
+ + +
+ ); +} diff --git a/packages/fiori/src/UserSettingsNotificationsViewTemplate.tsx b/packages/fiori/src/UserSettingsNotificationsViewTemplate.tsx new file mode 100644 index 0000000000000..ec820847c5cbf --- /dev/null +++ b/packages/fiori/src/UserSettingsNotificationsViewTemplate.tsx @@ -0,0 +1,19 @@ +import type UserSettingsNotificationsView from "./UserSettingsNotificationsView.js"; +import List from "@ui5/webcomponents/dist/List.js"; + +export default function UserSettingsNotificationsViewTemplate(this: UserSettingsNotificationsView) { + return ( + + ); +} diff --git a/packages/fiori/src/bundle.esm.ts b/packages/fiori/src/bundle.esm.ts index 12c4b81ad0184..8bcd67655e6c6 100644 --- a/packages/fiori/src/bundle.esm.ts +++ b/packages/fiori/src/bundle.esm.ts @@ -55,6 +55,9 @@ import UserSettingsAccountView from "./UserSettingsAccountView.js"; import UserSettingsAppearanceView from "./UserSettingsAppearanceView.js"; import UserSettingsAppearanceViewItem from "./UserSettingsAppearanceViewItem.js"; import UserSettingsAppearanceViewGroup from "./UserSettingsAppearanceViewGroup.js"; +import UserSettingsNotificationsView from "./UserSettingsNotificationsView.js"; +import UserSettingsNotificationsViewItem from "./UserSettingsNotificationsViewItem.js"; +import UserSettingsNotificationsViewGroup from "./UserSettingsNotificationsViewGroup.js"; import Timeline from "./Timeline.js"; import TimelineGroupItem from "./TimelineGroupItem.js"; import NavigationLayout from "./NavigationLayout.js"; diff --git a/packages/fiori/src/themes/UserSettingsNotificationsViewGroup.css b/packages/fiori/src/themes/UserSettingsNotificationsViewGroup.css new file mode 100644 index 0000000000000..f7a2959872a04 --- /dev/null +++ b/packages/fiori/src/themes/UserSettingsNotificationsViewGroup.css @@ -0,0 +1,13 @@ +::part(header) { + background: transparent; + color: var(--sapList_TextColor); + height: auto; + border-bottom: var(--sapList_BorderWidth) solid var(--sapList_GroupHeaderBorderColor); +} + +::part(title) { + font-family: var(--sapFontFamily); + font-size: var(--sapFontSize); + font-weight: bold; + color: var(--sapList_TextColor); +} diff --git a/packages/fiori/src/themes/UserSettingsNotificationsViewItem.css b/packages/fiori/src/themes/UserSettingsNotificationsViewItem.css new file mode 100644 index 0000000000000..4db00f0d4f50c --- /dev/null +++ b/packages/fiori/src/themes/UserSettingsNotificationsViewItem.css @@ -0,0 +1,98 @@ +:host { + --_ui5_user_settings_notifications_item_end_margin: 0.75rem; + min-height: var(--sapElement_LineHeight); +} + +@container style(--ui5_content_density: compact) { + :host { + --_ui5_user_settings_notifications_item_end_margin: 1rem; + min-height: var(--sapElement_Compact_LineHeight); + } +} + +.ui5-user-settings-notifications-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.ui5-user-settings-notifications-item-start { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + gap: 0.75rem; +} + +.ui5-user-settings-notifications-item-texts { + display: flex; + flex-direction: column; + min-width: 0; + overflow: hidden; +} + +.ui5-user-settings-notifications-item-title { + font-family: var(--sapFontFamily); + font-size: var(--sapFontLargeSize); + font-weight: normal; + color: var(--sapList_TextColor); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@container style(--ui5_content_density: compact) { + .ui5-user-settings-notifications-item-title { + font-size: var(--sapFontSize); + } +} + +.ui5-user-settings-notifications-item-byline { + font-family: var(--sapFontFamily); + font-size: var(--sapFontSize); + font-weight: normal; + color: var(--sapContent_LabelColor); + padding-top: 0.5rem; + padding-bottom: 0.125rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ui5-user-settings-notifications-item-end { + display: flex; + align-items: center; + flex-shrink: 0; + margin-inline-start: var(--_ui5_user_settings_notifications_item_end_margin); + padding-inline-end: 0.25rem; +} + +.ui5-user-settings-notifications-item-arrow { + color: var(--sapContent_IconColor); + width: 0.75rem; + height: 0.75rem; + margin-inline-start: 0.5rem; +} + +/* Byline variant: taller row, texts distributed top/bottom */ +.ui5-user-settings-notifications-item.has-byline { + align-items: stretch; + min-height: 5rem; +} + +@container style(--ui5_content_density: compact) { + .ui5-user-settings-notifications-item.has-byline { + min-height: 4rem; + } +} + +.has-byline .ui5-user-settings-notifications-item-start { + align-items: center; +} + +.has-byline .ui5-user-settings-notifications-item-title { + padding-top: 0.125rem; +} diff --git a/packages/fiori/test/pages/UserSettingsDialog.html b/packages/fiori/test/pages/UserSettingsDialog.html index 8a8f0a7ee375c..739d37ffb50ab 100644 --- a/packages/fiori/test/pages/UserSettingsDialog.html +++ b/packages/fiori/test/pages/UserSettingsDialog.html @@ -51,6 +51,13 @@ display: block; width: 100%; } + .ui5-user-settings-notifications-detail-description { + display: block; + padding: 0 0 1rem 0; + color: var(--sapContent_LabelColor); + font-size: var(--sapFontSize); + font-family: var(--sapFontFamily); + } @@ -249,251 +256,164 @@ - - - - -
-
- -
- SuccessFactors - Compensation | Learning | HR -
-
-
- - -
-
-
- -
-
- -
- S/4HANA | EMEA - Finance EMEA -
-
-
- - -
-
-
- -
-
- -
- S/4HANA | AJP - Finance AJP -
-
-
- - -
-
-
-
-
- - - -
-
-
- Allow Notifications -
-
-
- -
-
-
- - - Sales - - -
-
-
- Sales Order Updates -
-
-
- - -
-
-
- -
-
-
- Sales Order Approvals -
-
-
- - -
-
-
- -
-
-
- Sales Order Release -
-
-
- - -
-
-
-
- - Purchasing - - -
-
-
- Purchase Order Approval -
-
-
- - -
-
-
- -
-
-
- Purchase Order Rejection -
-
-
- - -
-
-
- -
-
-
- Purchase Order Overdue -
-
-
- - -
-
-
-
-
+ + + + Some settings are managed by your organization. Learn More + - - Sales Order Updates - - - - -
-
-
- Allow Sales Order Updates -
-
-
- -
-
-
-
- - Choose how you get notified - - -
-
-
- In-App Notification - Delivered inside the app -
-
-
- -
-
-
- -
-
-
- Allow Fly-Inn - Notifications fly in as banner from the figth se of the application while working -
-
-
- -
-
-
- -
-
-
- Push Notifications - Pushed to your device immediately -
-
-
- -
-
-
- -
-
-
- E-Mail - Send on your primary e-mail -
-
-
- -
-
-
- -
-
-
- Frequency - Pushed to your device immedietly -
-
-
- - - - - -
-
-
-
-
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + Immediate + Every 15 minutes + Hourly + Daily digest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Immediate + Every 15 minutes + Daily digest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -642,14 +562,7 @@ const toastReset = document.getElementById("toastReset"); const toastResetAll = document.getElementById("toastResetAll"); const notificationsItem = document.getElementById("notifications"); - const notificationNavIcon = document.querySelectorAll(".nav-icon"); - const notificationSwitch = document.querySelectorAll(".switch"); - const notificationSecondPage = document.getElementById("notification-second-page"); - const salesOrderList = document.querySelectorAll(".ui5-list-sales-order"); - - const salesOrderSecondPage = document.getElementById("sales-order-second-page"); - - const notificationList = document.querySelectorAll(".custom-list-main"); + const notificationsMainView = document.getElementById("notifications-main-view"); menuShellBar.addEventListener("item-click", function (event) { const item = event.detail.item.getAttribute("data-id"); @@ -755,62 +668,15 @@ //Notification Tab - notificationNavIcon.forEach(icon => { - icon.addEventListener("click", (e) => { - const title = e.target.closest(".list-item").querySelector(".item-title").textContent; - console.log("Navigate to: " + title +" icon clicked"); - notificationSecondPage.selected = true; - notificationSecondPage.text = title; - }); + notificationsMainView.addEventListener("item-click", (e) => { + console.log("Navigate to: " + e.detail.item.text); }); - notificationList.forEach(item => { - item.addEventListener("item-click", (e) => { - const title = e.detail?.item?.querySelector(".item-title")?.textContent; - console.log("Navigate to: " + title); - notificationSecondPage.selected = true; - notificationSecondPage.text = title; - }); - }); - - salesOrderList.forEach(item => { - item.addEventListener("item-click", (e) => { - const title = e.detail?.item?.querySelector(".sales-title")?.textContent; - console.log("Navigate to: " + title); - notificationSecondPage.selected = false; - salesOrderSecondPage.selected = true; - salesOrderSecondPage.text = title; - }); - }); - - notificationsItem.addEventListener("selection-change", (e) => { - if(e?.detail?.view?.id ==='sales-order-second-page'){ - e.preventDefault(); - console.log("Navigate back: " + notificationSecondPage.title); - salesOrderSecondPage.selected = false; - notificationSecondPage.selected = true; - } - }); - - notificationSwitch.forEach(sw => { - sw.addEventListener("change", (e) => { - var title = e.target.closest(".list-item").querySelector(".item-title")?.textContent; - if(!title){ - title = e.target?.closest(".list-item").querySelector(".item-text-title")?.textContent; - } - - const state = e.target.checked ? "ON" : "OFF"; - console.log(title + " switch changed to " + state); - }); - }); - - notificationList.forEach(item => { - item.addEventListener("item-click", (e) => { - const title = e.detail?.item?.querySelector(".item-title")?.textContent; - console.log("Navigate to: " + title); - notificationSecondPage.selected = true; - notificationSecondPage.text = title; - }); + notificationsItem.addEventListener("switch-change", (e) => { + const item = e.detail.item; + const view = item.closest("[ui5-user-settings-notifications-view]"); + const context = view?.text ? `${view.text} > ` : ""; + console.log(`${context}${item.text} switch changed to ${e.detail.checked ? "ON" : "OFF"}`); }); // Theme switching functionality diff --git a/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/main.js b/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/main.js index 309c4a26d2e76..0543725290ff1 100644 --- a/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/main.js +++ b/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/main.js @@ -2,6 +2,9 @@ import "@ui5/webcomponents-fiori/dist/UserSettingsAccountView.js"; import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceView.js"; import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceViewItem.js"; import "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceViewGroup.js"; +import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsView.js"; +import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsViewGroup.js"; +import "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsViewItem.js"; import "@ui5/webcomponents-fiori/dist/UserSettingsView.js"; import "@ui5/webcomponents-fiori/dist/UserSettingsItem.js"; import "@ui5/webcomponents-fiori/dist/UserSettingsDialog.js"; diff --git a/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/sample.html b/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/sample.html index 39fe370c120b3..1b928b1c216eb 100644 --- a/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/sample.html +++ b/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/sample.html @@ -205,9 +205,33 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/sample.tsx b/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/sample.tsx index 3a7b6686ff041..b35c055dd3b43 100644 --- a/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/sample.tsx +++ b/packages/website/docs/_samples/fiori/UserSettingsDialog/Basic/sample.tsx @@ -11,6 +11,9 @@ import UserSettingsAccountViewClass from "@ui5/webcomponents-fiori/dist/UserSett import UserSettingsAppearanceViewClass from "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceView.js"; import UserSettingsAppearanceViewGroupClass from "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceViewGroup.js"; import UserSettingsAppearanceViewItemClass from "@ui5/webcomponents-fiori/dist/UserSettingsAppearanceViewItem.js"; +import UserSettingsNotificationsViewClass from "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsView.js"; +import UserSettingsNotificationsViewGroupClass from "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsViewGroup.js"; +import UserSettingsNotificationsViewItemClass from "@ui5/webcomponents-fiori/dist/UserSettingsNotificationsViewItem.js"; import UserSettingsDialogClass from "@ui5/webcomponents-fiori/dist/UserSettingsDialog.js"; import UserSettingsItemClass from "@ui5/webcomponents-fiori/dist/UserSettingsItem.js"; import UserSettingsViewClass from "@ui5/webcomponents-fiori/dist/UserSettingsView.js"; @@ -50,6 +53,15 @@ const UserSettingsAppearanceViewGroup = createReactComponent( const UserSettingsAppearanceViewItem = createReactComponent( UserSettingsAppearanceViewItemClass, ); +const UserSettingsNotificationsView = createReactComponent( + UserSettingsNotificationsViewClass, +); +const UserSettingsNotificationsViewGroup = createReactComponent( + UserSettingsNotificationsViewGroupClass, +); +const UserSettingsNotificationsViewItem = createReactComponent( + UserSettingsNotificationsViewItemClass, +); const UserSettingsDialog = createReactComponent(UserSettingsDialogClass); const UserSettingsItem = createReactComponent(UserSettingsItemClass); const UserSettingsView = createReactComponent(UserSettingsViewClass); @@ -541,12 +553,66 @@ function App() { headerText="Notifications" onSelectionChange={handleSettingsDialogItemSelectionChange} > - - + - + + + + + + + + + + + + + + +
- - - + + + + + + + + + + + + + + From 42b726c531b3288efbb9c54db3d4c5744bd8a7e7 Mon Sep 17 00:00:00 2001 From: Dobromira Boycheva Date: Mon, 3 Aug 2026 14:43:02 +0300 Subject: [PATCH 2/7] fix(ui5-user-settings-notifications-view): align spacing, navigation, and samples with design spec --- .../UserSettingsNotificationsView.cy.tsx | 77 +++++++++++++++++++ .../src/UserSettingsNotificationsView.ts | 5 +- ...rSettingsNotificationsViewItemTemplate.tsx | 5 +- .../UserSettingsNotificationsViewTemplate.tsx | 3 +- .../themes/UserSettingsNotificationsView.css | 18 +++++ .../UserSettingsNotificationsViewGroup.css | 15 ++-- .../UserSettingsNotificationsViewItem.css | 1 - .../fiori/test/pages/UserSettingsDialog.html | 41 +++++++++- .../UserSettingsDialog/Basic/sample.html | 39 +++++++++- 9 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 packages/fiori/src/themes/UserSettingsNotificationsView.css diff --git a/packages/fiori/cypress/specs/UserSettingsNotificationsView.cy.tsx b/packages/fiori/cypress/specs/UserSettingsNotificationsView.cy.tsx index 82d4b9d227827..391e9222fa47c 100644 --- a/packages/fiori/cypress/specs/UserSettingsNotificationsView.cy.tsx +++ b/packages/fiori/cypress/specs/UserSettingsNotificationsView.cy.tsx @@ -809,5 +809,82 @@ describe("Notifications view — drill-in edge cases", () => { cy.get("#end-select").click(); cy.get("#drill").should("not.have.attr", "selected"); }); + + it("clicking the navigation arrow drills into the secondary view", () => { + cy.mount( + + + + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).shadow() + .find(".ui5-user-settings-notifications-item-arrow").click(); + cy.get("#drill").should("have.attr", "selected"); + }); + + it("does not drill in when there are no secondary sibling views", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view]").then($view => { + $view.get(0).addEventListener("item-click", cy.stub().as("itemClick")); + }); + + cy.get("[ui5-user-settings-notifications-view-item]").eq(0).click(); + cy.get("[ui5-user-settings-notifications-view]").should("not.have.attr", "selected"); + }); +}); + +describe("Notifications view item — checked state", () => { + it("updates checked on the item after the switch is toggled", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").as("item"); + cy.get("@item").shadow().find("[ui5-switch]").click(); + cy.get("@item").should("have.attr", "checked"); + + cy.get("@item").shadow().find("[ui5-switch]").click(); + cy.get("@item").should("not.have.attr", "checked"); + }); + + it("switch-change detail.checked matches the new item.checked state", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-user-settings-notifications-view-item]").as("item"); + cy.get("@item").then($item => { + $item.get(0).addEventListener("switch-change", cy.stub().as("changed")); + }); + + cy.get("@item").shadow().find("[ui5-switch]").click(); + cy.get("@changed").then((stub: any) => { + const call = stub.getCall(0); + expect(call.args[0].detail.checked).to.be.true; + cy.get("@item").then($item => { + expect(($item.get(0) as UserSettingsNotificationsViewItem).checked).to.be.true; + }); + }); + }); }); diff --git a/packages/fiori/src/UserSettingsNotificationsView.ts b/packages/fiori/src/UserSettingsNotificationsView.ts index 82cff1544c717..798743d85f88a 100644 --- a/packages/fiori/src/UserSettingsNotificationsView.ts +++ b/packages/fiori/src/UserSettingsNotificationsView.ts @@ -1,6 +1,7 @@ import UserSettingsView from "./UserSettingsView.js"; import UserSettingsNotificationsViewTemplate from "./UserSettingsNotificationsViewTemplate.js"; import UserSettingViewCss from "./generated/themes/UserSettingsView.css.js"; +import UserSettingsNotificationsViewCss from "./generated/themes/UserSettingsNotificationsView.css.js"; import type UserSettingsNotificationsViewItem from "./UserSettingsNotificationsViewItem.js"; import { isInstanceOfUserSettingsNotificationsViewItem } from "./UserSettingsNotificationsViewItem.js"; import type UserSettingsNotificationsViewGroup from "./UserSettingsNotificationsViewGroup.js"; @@ -55,7 +56,7 @@ type UserSettingsNotificationsViewItemClickEventDetail = { tag: "ui5-user-settings-notifications-view", renderer: jsxRenderer, template: UserSettingsNotificationsViewTemplate, - styles: [UserSettingViewCss], + styles: [UserSettingViewCss, UserSettingsNotificationsViewCss], }) /** @@ -143,7 +144,7 @@ class UserSettingsNotificationsView extends UserSettingsView { return; } - if (!matched) { + if (!matched || !target.text) { target.text = item.text; } diff --git a/packages/fiori/src/UserSettingsNotificationsViewItemTemplate.tsx b/packages/fiori/src/UserSettingsNotificationsViewItemTemplate.tsx index c550719c5e9f9..f5cb7761135f7 100644 --- a/packages/fiori/src/UserSettingsNotificationsViewItemTemplate.tsx +++ b/packages/fiori/src/UserSettingsNotificationsViewItemTemplate.tsx @@ -28,14 +28,15 @@ function listItemContent(this: UserSettingsNotificationsViewItem) { } -