diff --git a/packages/base/cypress/specs/UI5ElementSlots.cy.tsx b/packages/base/cypress/specs/UI5ElementSlots.cy.tsx
index 8be14d185cc1..0235e49a6669 100644
--- a/packages/base/cypress/specs/UI5ElementSlots.cy.tsx
+++ b/packages/base/cypress/specs/UI5ElementSlots.cy.tsx
@@ -133,4 +133,38 @@ describe("Slots work properly", () => {
.invoke("prop", "items")
.should("have.length", 3);
});
+
+ it("Tests that only changed slot triggers invalidation", () => {
+ cy.mount(
+
+ Default slot content
+ Other slot content 2
+
+ );
+
+ cy.get("[ui5-test-generic]")
+ .as("testGeneric");
+
+ cy.get("@testGeneric")
+ .should("be.visible");
+
+ cy.get("@testGeneric")
+ .then($el => {
+ cy.stub($el[0], "onInvalidation").as("invalidation");
+ });
+
+ cy.get("@invalidation")
+ .should("not.have.been.called");
+
+ cy.get("@testGeneric")
+ .then($el => {
+ const newEl = document.createElement("span");
+ newEl.innerText = "New Element";
+
+ $el.append(newEl);
+ });
+
+ cy.get("@invalidation")
+ .should("have.been.calledOnce");
+ });
});
diff --git a/packages/base/src/UI5Element.ts b/packages/base/src/UI5Element.ts
index 2d99b3597fcb..2328218f719b 100644
--- a/packages/base/src/UI5Element.ts
+++ b/packages/base/src/UI5Element.ts
@@ -251,6 +251,7 @@ abstract class UI5Element extends HTMLElement {
const ctor = this.constructor as typeof UI5Element;
if (ctor._needsShadowDOM()) {
const defaultOptions = { mode: "open" } as ShadowRootInit;
+
if (!this.shadowRoot) {
this.attachShadow({ ...defaultOptions, ...ctor.getMetadata().getShadowRootOptions() });
} else {
@@ -258,21 +259,6 @@ abstract class UI5Element extends HTMLElement {
// is inserted into the DOM declaratively using a tag.
this.__shouldHydrate = true;
}
-
- const slotsAreManaged = ctor.getMetadata().slotsAreManaged();
- if (slotsAreManaged) {
- this.shadowRoot!.addEventListener("slotchange", this._onShadowRootSlotChange.bind(this));
- }
- }
- }
-
- /**
- * Note: this "slotchange" listener is for slots, rendered in the component's shadow root
- */
- _onShadowRootSlotChange(e: Event) {
- const targetShadowRoot = (e.target as Node)?.getRootNode(); // the "slotchange" event target is always a slot element
- if (targetShadowRoot === this.shadowRoot) { // only for slotchange events that originate from slots, belonging to the component's shadow root
- this._processChildren();
}
}
@@ -409,12 +395,34 @@ abstract class UI5Element extends HTMLElement {
}
const canSlotText = metadata.canSlotText();
- const mutationObserverOptions = {
+ const mutationObserverOptions: MutationObserverInit = {
childList: true,
- subtree: canSlotText,
+ subtree: true,
characterData: canSlotText,
};
- observeDOMNode(this, this._processChildren.bind(this) as MutationCallback, mutationObserverOptions);
+ observeDOMNode(this, this.handleMutationChange.bind(this) as MutationCallback, mutationObserverOptions);
+ }
+
+ handleMutationChange(changes: MutationRecord[]) {
+ let shouldProccessChildren = false;
+
+ changes.forEach(change => {
+ // we observe all changes of the component except its slot attribute
+ if (change.target === this && change.type !== "attributes") {
+ shouldProccessChildren = true;
+ }
+
+ const directChildren = [...this.childNodes].includes(change.target as ChildNode);
+
+ // we observe slot attribute change only on the direct child of the web component
+ if (directChildren && change.type === "attributes" && change.attributeName === "slot") {
+ shouldProccessChildren = true;
+ }
+ });
+
+ if (shouldProccessChildren) {
+ this._processChildren();
+ }
}
/**