diff --git a/src/components/button-group/button-group.ts b/src/components/button-group/button-group.ts index 22da6027c..6f8f5d9e7 100644 --- a/src/components/button-group/button-group.ts +++ b/src/components/button-group/button-group.ts @@ -63,7 +63,7 @@ export default class IgcButtonGroupComponent extends EventEmitterMixin< const buttons = this.toggleButtons; const idx = buttons.indexOf( - added.length ? last(added).node : last(attributes) + added.length ? last(added).node : last(attributes).node ); for (const [i, button] of buttons.entries()) { diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index 5567fab16..46a146ec6 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -164,7 +164,7 @@ export default class IgcCarouselComponent extends EventEmitterMixin< return; } const idx = this.slides.indexOf( - added.length ? last(added).node : last(attributes) + added.length ? last(added).node : last(attributes).node ); for (const [i, slide] of this.slides.entries()) { diff --git a/src/components/common/controllers/mutation-observer.ts b/src/components/common/controllers/mutation-observer.ts index 846cc4082..7041b5f91 100644 --- a/src/components/common/controllers/mutation-observer.ts +++ b/src/components/common/controllers/mutation-observer.ts @@ -1,9 +1,8 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit'; - import { isElement } from '../util.js'; /** @ignore */ -export interface MutationControllerConfig { +export interface MutationControllerConfig { /** The callback function to run when a mutation occurs. */ callback: MutationControllerCallback; /** The underlying mutation observer configuration parameters. */ @@ -20,7 +19,7 @@ export interface MutationControllerConfig { filter?: MutationControllerFilter; } -type MutationControllerCallback = ( +type MutationControllerCallback = ( params: MutationControllerParams ) => unknown; @@ -28,19 +27,34 @@ type MutationControllerCallback = ( * Filter configuration to return elements that either match * an array of selector strings or a predicate function. */ -type MutationControllerFilter = string[] | ((node: T) => boolean); -type MutationDOMChange = { target: Element; node: T }; +type MutationControllerFilter = + | string[] + | ((node: T) => boolean); + +type MutationDOMChange = { + /** The parent of the added/removed element. */ + target: Element; + /** The added/removed element. */ + node: T; +}; + +type MutationAttributeChange = { + /** The host element of the changed attribute. */ + node: T; + /** The changed attribute name. */ + attributeName: string | null; +}; -type MutationChange = { +type MutationChange = { /** Elements that have attribute(s) changes. */ - attributes: T[]; + attributes: MutationAttributeChange[]; /** Elements that have been added. */ added: MutationDOMChange[]; /** Elements that have been removed. */ removed: MutationDOMChange[]; }; -export type MutationControllerParams = { +export type MutationControllerParams = { /** The original mutation records from the underlying observer. */ records: MutationRecord[]; /** The aggregated changes. */ @@ -49,25 +63,30 @@ export type MutationControllerParams = { observer: MutationController; }; -function mutationFilter(nodes: T[], filter?: MutationControllerFilter) { - if (!filter) { +function applyNodeFilter( + nodes: T[], + predicate?: MutationControllerFilter +): T[] { + if (!predicate) { return nodes; } - return Array.isArray(filter) - ? nodes.filter((node) => - filter.some((selector) => isElement(node) && node.matches(selector)) + return Array.isArray(predicate) + ? nodes.filter( + (node) => + isElement(node) && + predicate.some((selector) => node.matches(selector)) ) - : nodes.filter((node) => filter(node)); + : nodes.filter(predicate); } -class MutationController implements ReactiveController { - private _host: ReactiveControllerHost & Element; - private _observer: MutationObserver; - private _target: Element; - private _config: MutationObserverInit; - private _callback: MutationControllerCallback; - private _filter?: MutationControllerFilter; +class MutationController implements ReactiveController { + private readonly _host: ReactiveControllerHost & Element; + private readonly _observer: MutationObserver; + private readonly _target: Element; + private readonly _config: MutationObserverInit; + private readonly _callback: MutationControllerCallback; + private readonly _filter?: MutationControllerFilter; constructor( host: ReactiveControllerHost & Element, @@ -77,7 +96,7 @@ class MutationController implements ReactiveController { this._callback = options.callback; this._config = options.config; this._target = options.target ?? this._host; - this._filter = options.filter ?? []; + this._filter = options.filter; this._observer = new MutationObserver((records) => { this.disconnect(); @@ -88,36 +107,47 @@ class MutationController implements ReactiveController { host.addController(this); } - public hostConnected() { + /** @internal */ + public hostConnected(): void { this.observe(); } - public hostDisconnected() { + /** @internal */ + public hostDisconnected(): void { this.disconnect(); } private _process(records: MutationRecord[]): MutationControllerParams { + const predicate = this._filter; const changes: MutationChange = { attributes: [], added: [], removed: [], }; - const filter = this._filter; for (const record of records) { - if (record.type === 'attributes') { + const { type, target, attributeName, addedNodes, removedNodes } = record; + + if (type === 'attributes') { changes.attributes.push( - ...mutationFilter([record.target as T], filter) + ...applyNodeFilter([target as T], predicate).map((node) => ({ + node, + attributeName, + })) ); - } else if (record.type === 'childList') { + } else if (type === 'childList') { changes.added.push( - ...mutationFilter(Array.from(record.addedNodes) as T[], filter).map( - (node) => ({ target: record.target as Element, node }) - ) + ...applyNodeFilter([...addedNodes] as T[], predicate).map((node) => ({ + target: target as Element, + node, + })) ); changes.removed.push( - ...mutationFilter(Array.from(record.removedNodes) as T[], filter).map( - (node) => ({ target: record.target as Element, node }) + ...applyNodeFilter([...removedNodes] as T[], predicate).map( + (node) => ({ + target: target as Element, + node, + }) ) ); } @@ -130,12 +160,12 @@ class MutationController implements ReactiveController { * Begin receiving notifications of changes to the DOM based * on the configured {@link MutationControllerConfig.target|target} and observer {@link MutationControllerConfig.config|options}. */ - public observe() { + public observe(): void { this._observer.observe(this._target, this._config); } /** Stop watching for mutations. */ - public disconnect() { + public disconnect(): void { this._observer.disconnect(); } } @@ -149,9 +179,9 @@ class MutationController implements ReactiveController { * The mutation observer is disconnected before invoking the passed in callback and re-attached * after that in order to not loop itself in endless stream of changes. */ -export function createMutationController( +export function createMutationController( host: ReactiveControllerHost & Element, config: MutationControllerConfig -) { +): MutationController { return new MutationController(host, config); } diff --git a/src/components/select/select-group.ts b/src/components/select/select-group.ts index d9081c191..530f2f7cc 100644 --- a/src/components/select/select-group.ts +++ b/src/components/select/select-group.ts @@ -56,7 +56,7 @@ export default class IgcSelectGroupComponent extends LitElement { private _observerCallback({ changes: { attributes }, }: MutationControllerParams) { - for (const item of attributes) { + for (const { node: item } of attributes) { if (!this.disabled) { this.controlledItems = this.activeItems; } diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index cc3b12a18..f432ea68d 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -237,8 +237,8 @@ export default class IgcTabsComponent extends EventEmitterMixin< changes, }: MutationControllerParams): void { const selected = changes.attributes.find( - (tab) => this._tabs.includes(tab) && tab.selected - ); + ({ node: tab }) => this._tabs.includes(tab) && tab.selected + )?.node; this._setSelectedTab(selected, false); }