diff --git a/package.json b/package.json index 0e8d6708bb..bfac2a0954 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@custom-elements-manifest/to-markdown": "0.1.0", "@eslint/eslintrc": "3.1.0", "@eslint/js": "9.10.0", + "@lit-labs/observers": "2.0.3", "@lit-labs/router": "0.1.3", "@lit-labs/testing": "0.2.4", "@lit/react": "1.0.5", diff --git a/src/elements/autocomplete-grid/autocomplete-grid-button/autocomplete-grid-button.ts b/src/elements/autocomplete-grid/autocomplete-grid-button/autocomplete-grid-button.ts index 8438b74f88..9b0b42b059 100644 --- a/src/elements/autocomplete-grid/autocomplete-grid-button/autocomplete-grid-button.ts +++ b/src/elements/autocomplete-grid/autocomplete-grid-button/autocomplete-grid-button.ts @@ -1,3 +1,4 @@ +import { MutationController } from '@lit-labs/observers/mutation-controller.js'; import { type CSSResultGroup, isServer, type PropertyValues, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; @@ -6,7 +7,6 @@ import { hostAttributes, slotState } from '../../core/decorators.js'; import { setOrRemoveAttribute } from '../../core/dom.js'; import { isEventPrevented } from '../../core/eventing.js'; import { SbbDisabledMixin, SbbNegativeMixin } from '../../core/mixins.js'; -import { AgnosticMutationObserver } from '../../core/observers.js'; import { SbbIconNameMixin } from '../../icon.js'; import type { SbbAutocompleteGridOptionElement } from '../autocomplete-grid-option.js'; @@ -47,21 +47,27 @@ export class SbbAutocompleteGridButtonElement extends SbbDisabledMixin( /** Whether the component must be set disabled due disabled attribute on sbb-optgroup. */ private _disabledFromGroup = false; - /** MutationObserver on data attributes. */ - private _optionAttributeObserver = new AgnosticMutationObserver((mutationsList) => { - for (const mutation of mutationsList) { - if (mutation.attributeName === 'data-group-disabled') { - this._disabledFromGroup = this.hasAttribute('data-group-disabled'); - setOrRemoveAttribute(this, 'aria-disabled', `${this.disabled || this._disabledFromGroup}`); - } - } - }); - public constructor() { super(); if (!isServer) { this.setupBaseEventHandlers(); this.addEventListener('click', this._handleButtonClick); + + new MutationController(this, { + config: buttonObserverConfig, + callback: (mutationsList) => { + for (const mutation of mutationsList) { + if (mutation.attributeName === 'data-group-disabled') { + this._disabledFromGroup = this.hasAttribute('data-group-disabled'); + setOrRemoveAttribute( + this, + 'aria-disabled', + `${this.disabled || this._disabledFromGroup}`, + ); + } + } + }, + }); } } @@ -81,7 +87,6 @@ export class SbbAutocompleteGridButtonElement extends SbbDisabledMixin( this._disabledFromGroup = parentGroup.disabled; setOrRemoveAttribute(this, 'aria-disabled', `${this.disabled || this._disabledFromGroup}`); } - this._optionAttributeObserver.observe(this, buttonObserverConfig); } public override willUpdate(changedProperties: PropertyValues): void { @@ -91,11 +96,6 @@ export class SbbAutocompleteGridButtonElement extends SbbDisabledMixin( } } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._optionAttributeObserver.disconnect(); - } - private _handleButtonClick = async (event: MouseEvent): Promise => { if ((await isEventPrevented(event)) || !this.closest('form')) { return; diff --git a/src/elements/breadcrumb/breadcrumb-group/breadcrumb-group.ts b/src/elements/breadcrumb/breadcrumb-group/breadcrumb-group.ts index 4d5f381db6..b2126a8f76 100644 --- a/src/elements/breadcrumb/breadcrumb-group/breadcrumb-group.ts +++ b/src/elements/breadcrumb/breadcrumb-group/breadcrumb-group.ts @@ -1,8 +1,9 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import { type CSSResultGroup, html, - nothing, LitElement, + nothing, type PropertyValues, type TemplateResult, } from 'lit'; @@ -18,7 +19,6 @@ import { hostAttributes } from '../../core/decorators.js'; import { setOrRemoveAttribute } from '../../core/dom.js'; import { i18nBreadcrumbEllipsisButtonLabel } from '../../core/i18n.js'; import { SbbNamedSlotListMixin, type WithListChildren } from '../../core/mixins.js'; -import { AgnosticResizeObserver } from '../../core/observers.js'; import type { SbbBreadcrumbElement } from '../breadcrumb.js'; import style from './breadcrumb-group.scss?lit&inline'; @@ -52,7 +52,11 @@ export class SbbBreadcrumbGroupElement extends SbbNamedSlotListMixin< return this.getAttribute('data-state') as 'collapsed' | 'manually-expanded' | null; } - private _resizeObserver = new AgnosticResizeObserver(() => this._evaluateCollapsedState()); + private _resizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: () => this._evaluateCollapsedState(), + }); private _abort = new SbbConnectedAbortController(this); private _language = new SbbLanguageController(this); private _markForFocus = false; @@ -87,11 +91,6 @@ export class SbbBreadcrumbGroupElement extends SbbNamedSlotListMixin< this.toggleAttribute('data-loaded', true); } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._resizeObserver.disconnect(); - } - protected override willUpdate(changedProperties: PropertyValues>): void { super.willUpdate(changedProperties); @@ -168,7 +167,8 @@ export class SbbBreadcrumbGroupElement extends SbbNamedSlotListMixin< this.listChildren.length >= MIN_BREADCRUMBS_TO_COLLAPSE ) { this._state = 'collapsed'; - this._resizeObserver.disconnect(); + this._resizeObserver.hostDisconnected(); + this.removeController(this._resizeObserver); } } diff --git a/src/elements/card/common/card-action-common.ts b/src/elements/card/common/card-action-common.ts index 5cdc1d4633..71d14eda96 100644 --- a/src/elements/card/common/card-action-common.ts +++ b/src/elements/card/common/card-action-common.ts @@ -1,3 +1,4 @@ +import { MutationController } from '@lit-labs/observers/mutation-controller.js'; import type { CSSResultGroup, TemplateResult } from 'lit'; import { property } from 'lit/decorators.js'; import { html } from 'lit/static-html.js'; @@ -6,7 +7,6 @@ import { IS_FOCUSABLE_QUERY } from '../../core/a11y.js'; import type { SbbActionBaseElement } from '../../core/base-elements.js'; import { hostAttributes } from '../../core/decorators.js'; import type { AbstractConstructor } from '../../core/mixins.js'; -import { AgnosticMutationObserver } from '../../core/observers.js'; import type { SbbCardElement } from '../card.js'; import style from './card-action.scss?lit&inline'; @@ -47,9 +47,11 @@ export const SbbCardActionCommonElementMixin = < protected abstract actionRole: 'link' | 'button'; private _card: SbbCardElement | null = null; - private _cardMutationObserver = new AgnosticMutationObserver(() => - this._checkForSlottedActions(), - ); + private _cardMutationObserver = new MutationController(this, { + target: null, + config: { childList: true, subtree: true }, + callback: () => this._checkForSlottedActions(), + }); private _onActiveChange(): void { if (this._card) { @@ -79,10 +81,7 @@ export const SbbCardActionCommonElementMixin = < this._card.setAttribute('data-action-role', this.actionRole); this._checkForSlottedActions(); - this._cardMutationObserver.observe(this._card, { - childList: true, - subtree: true, - }); + this._cardMutationObserver.observe(this._card); } } @@ -97,7 +96,6 @@ export const SbbCardActionCommonElementMixin = < .forEach((el) => el.removeAttribute('data-card-focusable')); this._card = null; } - this._cardMutationObserver.disconnect(); } protected override renderTemplate(): TemplateResult { diff --git a/src/elements/container/sticky-bar/sticky-bar.ts b/src/elements/container/sticky-bar/sticky-bar.ts index 8f29c36171..af2ec52797 100644 --- a/src/elements/container/sticky-bar/sticky-bar.ts +++ b/src/elements/container/sticky-bar/sticky-bar.ts @@ -1,3 +1,4 @@ +import { IntersectionController } from '@lit-labs/observers/intersection-controller.js'; import { type CSSResultGroup, html, @@ -8,7 +9,6 @@ import { import { customElement, property } from 'lit/decorators.js'; import { hostAttributes } from '../../core/decorators.js'; -import { AgnosticIntersectionObserver } from '../../core/observers.js'; import style from './sticky-bar.scss?lit&inline'; @@ -34,9 +34,12 @@ export class SbbStickyBarElement extends LitElement { @property({ reflect: true }) public color?: 'white' | 'milk'; private _intersector?: HTMLSpanElement; - private _observer = new AgnosticIntersectionObserver((entries) => - this._toggleShadowVisibility(entries[0]), - ); + private _observer = new IntersectionController(this, { + // Although `this` is observed, we have to postpone observing + // into firstUpdated() to achieve a correct initial state. + target: null, + callback: (entries) => this._toggleShadowVisibility(entries[0]), + }); public override connectedCallback(): void { super.connectedCallback(); @@ -67,11 +70,6 @@ export class SbbStickyBarElement extends LitElement { ); } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._observer.disconnect(); - } - protected override render(): TemplateResult { return html`
diff --git a/src/elements/core/controllers/language-controller.ts b/src/elements/core/controllers/language-controller.ts index 69dd4f74ea..7636505a88 100644 --- a/src/elements/core/controllers/language-controller.ts +++ b/src/elements/core/controllers/language-controller.ts @@ -1,7 +1,6 @@ import { isServer, type ReactiveController, type ReactiveControllerHost } from 'lit'; import { readConfig } from '../config.js'; -import { AgnosticMutationObserver } from '../observers.js'; /** * The LanguageController is a reactive controller that observes the "lang" attribute @@ -19,11 +18,13 @@ export class SbbLanguageController implements ReactiveController { private static readonly _listeners = new Set(); /** MutationObserver that observes the "lang" attribute of the element. */ - private static readonly _observer = new AgnosticMutationObserver((mutations) => { - if (mutations[0].oldValue !== document.documentElement.getAttribute('lang')) { - SbbLanguageController._listeners.forEach((l) => l._callHandlers()); - } - }); + private static readonly _observer = !isServer + ? new MutationObserver((mutations) => { + if (mutations[0].oldValue !== document.documentElement.getAttribute('lang')) { + SbbLanguageController._listeners.forEach((l) => l._callHandlers()); + } + }) + : null; private static readonly _observerConfig = { attributeFilter: ['lang'], attributeOldValue: true, @@ -66,8 +67,11 @@ export class SbbLanguageController implements ReactiveController { } public hostConnected(): void { + if (isServer) { + return; + } if (!SbbLanguageController._listeners.size) { - SbbLanguageController._observer.observe( + SbbLanguageController._observer!.observe( document.documentElement, SbbLanguageController._observerConfig, ); @@ -80,10 +84,13 @@ export class SbbLanguageController implements ReactiveController { } public hostDisconnected(): void { + if (isServer) { + return; + } this._previousLanguage = this.current; SbbLanguageController._listeners.delete(this); if (!SbbLanguageController._listeners.size) { - SbbLanguageController._observer.disconnect(); + SbbLanguageController._observer!.disconnect(); } } diff --git a/src/elements/core/observers/intersection-observer.ts b/src/elements/core/observers/intersection-observer.ts index 52c464f9f1..8494fb700d 100644 --- a/src/elements/core/observers/intersection-observer.ts +++ b/src/elements/core/observers/intersection-observer.ts @@ -1,3 +1,6 @@ +/** + * @deprecated use lit observers, will be removed with next major version + */ export class NodeIntersectionObserver implements IntersectionObserver { public root!: Element | Document | null; public rootMargin!: string; @@ -20,6 +23,9 @@ export class NodeIntersectionObserver implements IntersectionObserver { } } +/** + * @deprecated use lit observers, will be removed with next major version + */ // eslint-disable-next-line @typescript-eslint/naming-convention export const AgnosticIntersectionObserver = typeof IntersectionObserver === 'undefined' ? NodeIntersectionObserver : IntersectionObserver; diff --git a/src/elements/core/observers/mutation-observer.ts b/src/elements/core/observers/mutation-observer.ts index 0ad99dcac4..e09862fd42 100644 --- a/src/elements/core/observers/mutation-observer.ts +++ b/src/elements/core/observers/mutation-observer.ts @@ -1,3 +1,6 @@ +/** + * @deprecated use lit observers, will be removed with next major version + */ export class NodeMutationObserver implements MutationObserver { public disconnect(): void { // Noop @@ -12,6 +15,9 @@ export class NodeMutationObserver implements MutationObserver { } } +/** + * @deprecated use lit observers, will be removed with next major version + */ // eslint-disable-next-line @typescript-eslint/naming-convention export const AgnosticMutationObserver: typeof MutationObserver = typeof MutationObserver === 'undefined' ? NodeMutationObserver : MutationObserver; diff --git a/src/elements/core/observers/resize-observer.ts b/src/elements/core/observers/resize-observer.ts index 33aa7f98f7..c11f4ba04e 100644 --- a/src/elements/core/observers/resize-observer.ts +++ b/src/elements/core/observers/resize-observer.ts @@ -1,3 +1,6 @@ +/** + * @deprecated use lit observers, will be removed with next major version + */ export class NodeResizeObserver implements ResizeObserver { public disconnect(): any { // noop @@ -12,6 +15,9 @@ export class NodeResizeObserver implements ResizeObserver { } } +/** + * @deprecated use lit observers, will be removed with next major version + */ // eslint-disable-next-line @typescript-eslint/naming-convention export const AgnosticResizeObserver = typeof ResizeObserver === 'undefined' ? NodeResizeObserver : ResizeObserver; diff --git a/src/elements/datepicker/datepicker/datepicker.ts b/src/elements/datepicker/datepicker/datepicker.ts index 46e8d1eb20..8a8c735c88 100644 --- a/src/elements/datepicker/datepicker/datepicker.ts +++ b/src/elements/datepicker/datepicker/datepicker.ts @@ -1,5 +1,11 @@ -import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; -import { html, LitElement } from 'lit'; +import { + type CSSResultGroup, + html, + isServer, + LitElement, + type PropertyValues, + type TemplateResult, +} from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { readConfig } from '../../core/config.js'; @@ -9,7 +15,6 @@ import { findInput, findReferencedElement } from '../../core/dom.js'; import { EventEmitter } from '../../core/eventing.js'; import { i18nDateChangedTo, i18nDatePickerPlaceholder } from '../../core/i18n.js'; import type { SbbDateLike, SbbValidationChangeEvent } from '../../core/interfaces.js'; -import { AgnosticMutationObserver } from '../../core/observers.js'; import type { SbbDatepickerButton } from '../common.js'; import type { SbbDatepickerToggleElement } from '../datepicker-toggle.js'; @@ -276,14 +281,16 @@ export class SbbDatepickerElement extends LitElement { private _datePickerController!: AbortController; - private _inputObserver = new AgnosticMutationObserver((mutationsList) => { - this._emitInputUpdated(); - // TODO: Decide whether to remove this logic by adding a value property to the datepicker. - if (this._inputElement && mutationsList?.some((e) => e.attributeName === 'value')) { - const value = this._inputElement.getAttribute('value'); - this.valueAsDate = this._dateAdapter.parse(value, this.now) ?? value; - } - }); + private _inputObserver = !isServer + ? new MutationObserver((mutationsList) => { + this._emitInputUpdated(); + // TODO: Decide whether to remove this logic by adding a value property to the datepicker. + if (this._inputElement && mutationsList?.some((e) => e.attributeName === 'value')) { + const value = this._inputElement.getAttribute('value'); + this.valueAsDate = this._dateAdapter.parse(value, this.now) ?? value; + } + }) + : null; private _dateAdapter: DateAdapter = readConfig().datetime?.dateAdapter ?? defaultDateAdapter; @@ -375,7 +382,7 @@ export class SbbDatepickerElement extends LitElement { this._inputElement = input; if (input) { this._datePickerController = new AbortController(); - this._inputObserver.observe(input, { + this._inputObserver?.observe(input, { attributeFilter: ['disabled', 'readonly', 'min', 'max', 'value'], }); diff --git a/src/elements/dialog/dialog/dialog.ts b/src/elements/dialog/dialog/dialog.ts index b99c60d759..562e0961ce 100644 --- a/src/elements/dialog/dialog/dialog.ts +++ b/src/elements/dialog/dialog/dialog.ts @@ -1,10 +1,10 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { html } from 'lit/static-html.js'; import { getFirstFocusableElement, setModalityOnNextFocus } from '../../core/a11y.js'; import { isBreakpoint } from '../../core/dom.js'; -import { AgnosticResizeObserver } from '../../core/observers.js'; import { overlayRefs, SbbOverlayBaseElement } from '../../overlay.js'; import type { SbbDialogActionsElement } from '../dialog-actions.js'; import type { SbbDialogTitleElement } from '../dialog-title.js'; @@ -38,9 +38,11 @@ export class SbbDialogElement extends SbbOverlayBaseElement { // For more details: // - https://github.com/WICG/resize-observer/issues/38#issuecomment-422126006 // - https://github.com/juggle/resize-observer/issues/103#issuecomment-1711148285 - private _dialogContentResizeObserver = new AgnosticResizeObserver(() => - setTimeout(() => this._onContentResize()), - ); + private _dialogContentResizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: () => setTimeout(() => this._onContentResize()), + }); private _dialogTitleElement: SbbDialogTitleElement | null = null; private _dialogTitleHeight?: number; @@ -107,11 +109,6 @@ export class SbbDialogElement extends SbbOverlayBaseElement { } } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._dialogContentResizeObserver.disconnect(); - } - protected override attachOpenOverlayEvents(): void { super.attachOpenOverlayEvents(); @@ -152,7 +149,9 @@ export class SbbDialogElement extends SbbOverlayBaseElement { this.lastFocusedElement?.focus(); this.openOverlayController?.abort(); this.focusHandler.disconnect(); - this._dialogContentResizeObserver.disconnect(); + if (this._dialogContentElement) { + this._dialogContentResizeObserver.unobserve(this._dialogContentElement); + } this.removeInstanceFromGlobalCollection(); // Enable scrolling for content below the dialog if no dialog is open if (!overlayRefs.length) { diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.spec.ts b/src/elements/flip-card/flip-card-details/flip-card-details.spec.ts index 57269d2f58..c3826b7eec 100644 --- a/src/elements/flip-card/flip-card-details/flip-card-details.spec.ts +++ b/src/elements/flip-card/flip-card-details/flip-card-details.spec.ts @@ -1,7 +1,8 @@ -import { assert } from '@open-wc/testing'; +import { assert, expect } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; import { fixture } from '../../core/testing/private.js'; +import { waitForLitRender } from '../../core/testing/wait-for-render.js'; import { SbbFlipCardDetailsElement } from './flip-card-details.js'; @@ -9,10 +10,26 @@ describe('sbb-flip-card-details', () => { let element: SbbFlipCardDetailsElement; beforeEach(async () => { - element = await fixture(html``); + element = await fixture( + html` + Content + Link + `, + ); }); it('renders', async () => { assert.instanceOf(element, SbbFlipCardDetailsElement); }); + + it('should have data-card-focusable attribute', async () => { + expect(element.querySelector('a')).to.have.attribute('data-card-focusable'); + }); + + it('should set data-card-focusable on a newly slotted action', async () => { + element.append(document.createElement('button')); + await waitForLitRender(element); + + expect(element.querySelector('button')).to.have.attribute('data-card-focusable'); + }); }); diff --git a/src/elements/flip-card/flip-card-details/flip-card-details.ts b/src/elements/flip-card/flip-card-details/flip-card-details.ts index 85b2da2cf5..49584d3f47 100644 --- a/src/elements/flip-card/flip-card-details/flip-card-details.ts +++ b/src/elements/flip-card/flip-card-details/flip-card-details.ts @@ -1,10 +1,10 @@ -import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; +import { MutationController } from '@lit-labs/observers/mutation-controller.js'; +import type { CSSResultGroup, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; import { IS_FOCUSABLE_QUERY } from '../../core/a11y.js'; import { hostAttributes } from '../../core/decorators.js'; -import { AgnosticMutationObserver } from '../../core/observers.js'; import style from './flip-card-details.scss?lit&inline'; @@ -20,9 +20,17 @@ import style from './flip-card-details.scss?lit&inline'; export class SbbFlipCardDetailsElement extends LitElement { public static override styles: CSSResultGroup = style; - private _flipCardMutationObserver = new AgnosticMutationObserver(() => - this._checkForSlottedActions(), - ); + public constructor() { + super(); + + new MutationController(this, { + config: { + childList: true, + subtree: true, + }, + callback: () => this._checkForSlottedActions(), + }); + } private _checkForSlottedActions(): void { const cardFocusableAttributeName = 'data-card-focusable'; @@ -32,25 +40,6 @@ export class SbbFlipCardDetailsElement extends LitElement { .forEach((el: Element) => el.setAttribute(cardFocusableAttributeName, '')); } - public override connectedCallback(): void { - super.connectedCallback(); - this._checkForSlottedActions(); - this._flipCardMutationObserver.observe(this, { - childList: true, - subtree: true, - }); - } - - protected override firstUpdated(changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - this._checkForSlottedActions(); - } - - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._flipCardMutationObserver.disconnect(); - } - protected override render(): TemplateResult { return html`
diff --git a/src/elements/form-field/form-field/form-field.ts b/src/elements/form-field/form-field/form-field.ts index a58b86d4cb..31906d5092 100644 --- a/src/elements/form-field/form-field/form-field.ts +++ b/src/elements/form-field/form-field/form-field.ts @@ -1,4 +1,4 @@ -import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; +import { type CSSResultGroup, isServer, type PropertyValues, type TemplateResult } from 'lit'; import { html, LitElement, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; @@ -9,7 +9,6 @@ import { slotState } from '../../core/decorators.js'; import { isFirefox, setOrRemoveAttribute } from '../../core/dom.js'; import { i18nOptional } from '../../core/i18n.js'; import { SbbHydrationMixin, SbbNegativeMixin } from '../../core/mixins.js'; -import { AgnosticMutationObserver } from '../../core/observers.js'; import type { SbbSelectElement } from '../../select.js'; import style from './form-field.scss?lit&inline'; @@ -114,13 +113,13 @@ export class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitE /** * Listens to the changes on `readonly` and `disabled` attributes of ``. */ - private _formFieldAttributeObserver = new AgnosticMutationObserver( - (mutations: MutationRecord[]) => { - if (mutations.some((m) => m.type === 'attributes')) { - this._readInputState(); - } - }, - ); + private _formFieldAttributeObserver = !isServer + ? new MutationObserver((mutations: MutationRecord[]) => { + if (mutations.some((m) => m.type === 'attributes')) { + this._readInputState(); + } + }) + : null; private _inputAbortController = new AbortController(); @@ -143,7 +142,7 @@ export class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitE public override disconnectedCallback(): void { super.disconnectedCallback(); - this._formFieldAttributeObserver.disconnect(); + this._formFieldAttributeObserver?.disconnect(); this._inputAbortController.abort(); } @@ -215,8 +214,8 @@ export class SbbFormFieldElement extends SbbNegativeMixin(SbbHydrationMixin(LitE this._input.setAttribute('rows', this._input.getAttribute('rows') || '3'); } - this._formFieldAttributeObserver.disconnect(); - this._formFieldAttributeObserver.observe(this._input, { + this._formFieldAttributeObserver?.disconnect(); + this._formFieldAttributeObserver?.observe(this._input, { attributes: true, attributeFilter: ['readonly', 'disabled', 'class', 'data-sbb-invalid'], }); diff --git a/src/elements/map-container/__snapshots__/map-container.snapshot.spec.snap.js b/src/elements/map-container/__snapshots__/map-container.snapshot.spec.snap.js index b274e58bc9..ec5ed18bb1 100644 --- a/src/elements/map-container/__snapshots__/map-container.snapshot.spec.snap.js +++ b/src/elements/map-container/__snapshots__/map-container.snapshot.spec.snap.js @@ -14,7 +14,7 @@ snapshots["sbb-map-container renders Shadow DOM"] =
- + @@ -52,6 +52,8 @@ snapshots["sbb-map-container renders without scroll-up button Shadow DOM"] =
+ +
diff --git a/src/elements/map-container/map-container.ts b/src/elements/map-container/map-container.ts index c8575987a3..b8c23268ef 100644 --- a/src/elements/map-container/map-container.ts +++ b/src/elements/map-container/map-container.ts @@ -1,12 +1,10 @@ -import type { CSSResultGroup, TemplateResult } from 'lit'; +import { IntersectionController } from '@lit-labs/observers/intersection-controller.js'; +import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; -import { ref } from 'lit/directives/ref.js'; -import type { SbbTertiaryButtonElement } from '../button.js'; import { SbbLanguageController } from '../core/controllers.js'; import { i18nMapContainerButtonLabel } from '../core/i18n.js'; -import { AgnosticIntersectionObserver } from '../core/observers.js'; import style from './map-container.scss?lit&inline'; @@ -33,11 +31,36 @@ export class SbbMapContainerElement extends LitElement { @state() private _scrollUpButtonVisible = false; - private _intersector?: HTMLSpanElement; private _language = new SbbLanguageController(this); - private _observer = new AgnosticIntersectionObserver((entries) => - this._toggleButtonVisibilityOnIntersect(entries), - ); + private _observer = new IntersectionController(this, { + target: null, + callback: (entries) => this._toggleButtonVisibilityOnIntersect(entries), + }); + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + if (changedProperties.has('hideScrollUpButton')) { + const intersectorElement = this._intersector(); + + if (!this.hideScrollUpButton && intersectorElement) { + this._observer.observe(intersectorElement); + } else if (intersectorElement) { + this._observer.unobserve(intersectorElement); + } + } + } + + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + if (!this.hideScrollUpButton) { + this._observer.observe(this._intersector()!); + } + } + + private _intersector(): HTMLElement | null { + return this.shadowRoot?.querySelector('#intersector') ?? null; + } /** * Button click callback to trigger the scroll to container top @@ -46,6 +69,7 @@ export class SbbMapContainerElement extends LitElement { private _onScrollButtonClick(): void { this.scrollIntoView({ behavior: 'smooth' }); } + /** * Intersection callback. Toggles the visibility. * @param entries @@ -59,23 +83,6 @@ export class SbbMapContainerElement extends LitElement { }); } - public override connectedCallback(): void { - super.connectedCallback(); - this._updateIntersectionObserver(); - } - - private _updateIntersectionObserver(): void { - this._observer.disconnect(); - if (!this.hideScrollUpButton && this._intersector) { - this._observer.observe(this._intersector); - } - } - - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._observer.disconnect(); - } - protected override render(): TemplateResult { return html`
@@ -83,17 +90,7 @@ export class SbbMapContainerElement extends LitElement {
- ${!this.hideScrollUpButton - ? html` { - if (this._intersector === el) { - return; - } - this._intersector = el as HTMLSpanElement; - this._updateIntersectionObserver(); - })} - >` - : nothing} + @@ -104,11 +101,7 @@ export class SbbMapContainerElement extends LitElement { icon-name="location-pin-map-small" type="button" @click=${() => this._onScrollButtonClick()} - ${ref((ref?: Element) => { - if (ref) { - (ref as SbbTertiaryButtonElement).inert = !this._scrollUpButtonVisible; - } - })} + .inert=${!this._scrollUpButtonVisible} > ${i18nMapContainerButtonLabel[this._language.current]} ` diff --git a/src/elements/navigation/navigation-marker/navigation-marker.ts b/src/elements/navigation/navigation-marker/navigation-marker.ts index 3ca9b7fda3..b151932505 100644 --- a/src/elements/navigation/navigation-marker/navigation-marker.ts +++ b/src/elements/navigation/navigation-marker/navigation-marker.ts @@ -1,8 +1,8 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import { type CSSResultGroup, LitElement, type PropertyValues, type TemplateResult } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { SbbNamedSlotListMixin, type WithListChildren } from '../../core/mixins.js'; -import { AgnosticResizeObserver } from '../../core/observers.js'; import type { SbbNavigationButtonElement } from '../navigation-button.js'; import type { SbbNavigationLinkElement } from '../navigation-link.js'; @@ -29,11 +29,16 @@ export class SbbNavigationMarkerElement extends SbbNamedSlotListMixin< */ @property({ reflect: true }) public size?: 'l' | 's' = 'l'; - @state() private _currentActiveAction?: SbbNavigationButtonElement | SbbNavigationLinkElement; + private _currentActiveAction?: SbbNavigationButtonElement | SbbNavigationLinkElement; - private _navigationMarkerResizeObserver = new AgnosticResizeObserver(() => - this._setMarkerPosition(), - ); + public constructor() { + super(); + + new ResizeController(this, { + skipInitial: true, + callback: () => this._setMarkerPosition(), + }); + } protected override willUpdate(changedProperties: PropertyValues>): void { super.willUpdate(changedProperties); @@ -57,7 +62,6 @@ export class SbbNavigationMarkerElement extends SbbNamedSlotListMixin< public override connectedCallback(): void { super.connectedCallback(); - this._navigationMarkerResizeObserver.observe(this); this._checkActiveAction(); } @@ -70,11 +74,6 @@ export class SbbNavigationMarkerElement extends SbbNamedSlotListMixin< } } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._navigationMarkerResizeObserver.disconnect(); - } - public select(action: SbbNavigationButtonElement | SbbNavigationLinkElement): void { if (!action) { return; diff --git a/src/elements/navigation/navigation/navigation.ts b/src/elements/navigation/navigation/navigation.ts index 668e78c818..45b44aaa5e 100644 --- a/src/elements/navigation/navigation/navigation.ts +++ b/src/elements/navigation/navigation/navigation.ts @@ -1,3 +1,5 @@ +import { MutationController } from '@lit-labs/observers/mutation-controller.js'; +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import type { CSSResultGroup, TemplateResult } from 'lit'; import { html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; @@ -14,7 +16,6 @@ import { hostAttributes } from '../../core/decorators.js'; import { findReferencedElement, SbbScrollHandler } from '../../core/dom.js'; import { i18nCloseNavigation } from '../../core/i18n.js'; import { SbbUpdateSchedulerMixin } from '../../core/mixins.js'; -import { AgnosticMutationObserver, AgnosticResizeObserver } from '../../core/observers.js'; import { isEventOnElement, removeAriaOverlayTriggerAttributes, @@ -98,10 +99,20 @@ export class SbbNavigationElement extends SbbUpdateSchedulerMixin(SbbOpenCloseBa private _scrollHandler = new SbbScrollHandler(); private _isPointerDownEventOnNavigation: boolean = false; private _resizeObserverTimeout: ReturnType | null = null; - private _navigationObserver = new AgnosticMutationObserver((mutationsList: MutationRecord[]) => - this._onNavigationSectionChange(mutationsList), - ); - private _navigationResizeObserver = new AgnosticResizeObserver(() => this._onNavigationResize()); + private _navigationResizeObserver = new ResizeController(this, { + skipInitial: true, + callback: () => this._onNavigationResize(), + }); + + public constructor() { + super(); + + new MutationController(this, { + skipInitial: true, + config: navigationObserverConfig, + callback: (mutationsList: MutationRecord[]) => this._onNavigationSectionChange(mutationsList), + }); + } /** * Opens the navigation. @@ -326,7 +337,6 @@ export class SbbNavigationElement extends SbbUpdateSchedulerMixin(SbbOpenCloseBa this.addEventListener('click', (e) => this._handleNavigationClose(e), { signal }); // Validate trigger element and attach event listeners this._configure(this.trigger); - this._navigationObserver.observe(this, navigationObserverConfig); this.addEventListener('pointerup', (event) => this._closeOnBackdropClick(event), { signal }); this.addEventListener('pointerdown', (event) => this._pointerDownListener(event), { signal }); } @@ -336,8 +346,6 @@ export class SbbNavigationElement extends SbbUpdateSchedulerMixin(SbbOpenCloseBa this._navigationController?.abort(); this._windowEventsController?.abort(); this._focusHandler.disconnect(); - this._navigationObserver.disconnect(); - this._navigationResizeObserver.disconnect(); this._scrollHandler.enableScroll(); } diff --git a/src/elements/notification/notification.ts b/src/elements/notification/notification.ts index 624147a741..e868b5d8ca 100644 --- a/src/elements/notification/notification.ts +++ b/src/elements/notification/notification.ts @@ -1,3 +1,4 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement, nothing } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @@ -7,7 +8,6 @@ import { slotState } from '../core/decorators.js'; import { EventEmitter } from '../core/eventing.js'; import { i18nCloseNotification } from '../core/i18n.js'; import type { SbbOpenedClosedState } from '../core/interfaces.js'; -import { AgnosticResizeObserver } from '../core/observers.js'; import type { SbbTitleLevel } from '../title.js'; import style from './notification.scss?lit&inline'; @@ -82,9 +82,11 @@ export class SbbNotificationElement extends LitElement { private _notificationElement!: HTMLElement; private _resizeObserverTimeout: ReturnType | null = null; private _language = new SbbLanguageController(this); - private _notificationResizeObserver = new AgnosticResizeObserver(() => - this._onNotificationResize(), - ); + private _notificationResizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: () => this._onNotificationResize(), + }); /** Emits whenever the `sbb-notification` starts the opening transition. */ private _willOpen: EventEmitter = new EventEmitter( @@ -143,11 +145,6 @@ export class SbbNotificationElement extends LitElement { this._open(); } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._notificationResizeObserver.disconnect(); - } - private _setNotificationHeight(): void { if (!this._notificationElement?.scrollHeight) { return; diff --git a/src/elements/option/optgroup/optgroup-base-element.ts b/src/elements/option/optgroup/optgroup-base-element.ts index c5d0ac000c..31b63df3cf 100644 --- a/src/elements/option/optgroup/optgroup-base-element.ts +++ b/src/elements/option/optgroup/optgroup-base-element.ts @@ -1,3 +1,4 @@ +import { MutationController } from '@lit-labs/observers/mutation-controller.js'; import { type CSSResultGroup, html, @@ -11,7 +12,6 @@ import type { SbbAutocompleteBaseElement } from '../../autocomplete.js'; import { hostAttributes } from '../../core/decorators.js'; import { isSafari, setOrRemoveAttribute } from '../../core/dom.js'; import { SbbDisabledMixin, SbbHydrationMixin } from '../../core/mixins.js'; -import { AgnosticMutationObserver } from '../../core/observers.js'; import type { SbbOptionBaseElement } from '../option.js'; import style from './optgroup-base-element.scss?lit&inline'; @@ -38,13 +38,19 @@ export abstract class SbbOptgroupBaseElement extends SbbDisabledMixin( @state() private _inertAriaGroups = false; - private _negativeObserver = new AgnosticMutationObserver(() => this._onNegativeChange()); - protected abstract get options(): SbbOptionBaseElement[]; public constructor() { super(); + new MutationController(this, { + config: { + attributes: true, + attributeFilter: ['data-negative'], + }, + callback: () => this._onNegativeChange(), + }); + if (inertAriaGroups) { if (this.hydrationRequired) { this.hydrationComplete.then(() => (this._inertAriaGroups = inertAriaGroups)); @@ -56,13 +62,7 @@ export abstract class SbbOptgroupBaseElement extends SbbDisabledMixin( public override connectedCallback(): void { super.connectedCallback(); - this._negativeObserver?.disconnect(); this.setAttributeFromParent(); - this._negativeObserver.observe(this, { - attributes: true, - attributeFilter: ['data-negative'], - }); - this._proxyGroupLabelToOptions(); } @@ -81,11 +81,6 @@ export abstract class SbbOptgroupBaseElement extends SbbDisabledMixin( } } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._negativeObserver?.disconnect(); - } - protected abstract setAttributeFromParent(): void; protected abstract getAutocompleteParent(): SbbAutocompleteBaseElement | null; diff --git a/src/elements/option/option/option-base-element.ts b/src/elements/option/option/option-base-element.ts index b215b620e4..b51b1a3fe1 100644 --- a/src/elements/option/option/option-base-element.ts +++ b/src/elements/option/option/option-base-element.ts @@ -1,3 +1,4 @@ +import { MutationController } from '@lit-labs/observers/mutation-controller.js'; import { html, LitElement, nothing, type PropertyValues, type TemplateResult } from 'lit'; import { property, state } from 'lit/decorators.js'; @@ -6,8 +7,8 @@ import { slotState } from '../../core/decorators.js'; import { isAndroid, isSafari, setOrRemoveAttribute } from '../../core/dom.js'; import type { EventEmitter } from '../../core/eventing.js'; import { SbbDisabledMixin, SbbHydrationMixin } from '../../core/mixins.js'; -import { AgnosticMutationObserver } from '../../core/observers.js'; import { SbbIconNameMixin } from '../../icon.js'; + import '../../screen-reader-only.js'; let nextId = 0; @@ -86,14 +87,14 @@ export abstract class SbbOptionBaseElement extends SbbDisabledMixin( private _abort = new SbbConnectedAbortController(this); - /** MutationObserver on data attributes. */ - private _optionAttributeObserver = new AgnosticMutationObserver((mutationsList) => - this.onOptionAttributesChange(mutationsList), - ); - public constructor() { super(); + new MutationController(this, { + config: optionObserverConfig, + callback: (mutationsList) => this.onOptionAttributesChange(mutationsList), + }); + if (inertAriaGroups) { if (this.hydrationRequired) { this.hydrationComplete.then(() => (this._inertAriaGroups = inertAriaGroups)); @@ -159,11 +160,6 @@ export abstract class SbbOptionBaseElement extends SbbDisabledMixin( this._updateAriaSelected(); } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._optionAttributeObserver.disconnect(); - } - protected abstract selectByClick(event: MouseEvent): void; protected abstract setAttributeFromParent(): void; @@ -182,7 +178,6 @@ export abstract class SbbOptionBaseElement extends SbbDisabledMixin( protected init(): void { this.setAttributeFromParent(); - this._optionAttributeObserver.observe(this, optionObserverConfig); const signal = this._abort.signal; this.addEventListener('click', (e: MouseEvent) => this.selectByClick(e), { signal, diff --git a/src/elements/package.json b/src/elements/package.json index 4fae1771c6..df3e6bce02 100644 --- a/src/elements/package.json +++ b/src/elements/package.json @@ -11,6 +11,7 @@ "type": "module", "customElements": "custom-elements.json", "dependencies": { + "@lit-labs/observers": "0.0.0-LITOBSERVERS", "lit": "0.0.0-LIT" }, "publishConfig": { diff --git a/src/elements/selection-expansion-panel/selection-expansion-panel.ts b/src/elements/selection-expansion-panel/selection-expansion-panel.ts index 4e8922e8c1..623f8cf61a 100644 --- a/src/elements/selection-expansion-panel/selection-expansion-panel.ts +++ b/src/elements/selection-expansion-panel/selection-expansion-panel.ts @@ -1,4 +1,4 @@ -import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; +import { type CSSResultGroup, isServer, type PropertyValues, type TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; @@ -9,7 +9,6 @@ import { EventEmitter } from '../core/eventing.js'; import { i18nCollapsed, i18nExpanded } from '../core/i18n.js'; import type { SbbOpenedClosedState, SbbStateChange } from '../core/interfaces.js'; import { SbbHydrationMixin } from '../core/mixins.js'; -import { AgnosticMutationObserver } from '../core/observers.js'; import type { SbbRadioButtonGroupElement, SbbRadioButtonPanelElement } from '../radio-button.js'; import style from './selection-expansion-panel.scss?lit&inline'; @@ -96,9 +95,11 @@ export class SbbSelectionExpansionPanelElement extends SbbHydrationMixin(LitElem private _language = new SbbLanguageController(this); private _abort = new SbbConnectedAbortController(this); private _initialized: boolean = false; - private _sizeAttributeObserver = new AgnosticMutationObserver((mutationsList: MutationRecord[]) => - this._onSizeAttributesChange(mutationsList), - ); + private _sizeAttributeObserver = !isServer + ? new MutationObserver((mutationsList: MutationRecord[]) => + this._onSizeAttributesChange(mutationsList), + ) + : null; /** Whether it has an expandable content */ private get _hasContent(): boolean { @@ -124,7 +125,7 @@ export class SbbSelectionExpansionPanelElement extends SbbHydrationMixin(LitElem public override disconnectedCallback(): void { super.disconnectedCallback(); - this._sizeAttributeObserver.disconnect(); + this._sizeAttributeObserver?.disconnect(); } protected override willUpdate(changedProperties: PropertyValues): void { @@ -182,9 +183,9 @@ export class SbbSelectionExpansionPanelElement extends SbbHydrationMixin(LitElem this._checked = input.checked; this._disabled = input.disabled; - this._sizeAttributeObserver.disconnect(); + this._sizeAttributeObserver?.disconnect(); // The size of the inner panel can change due direct change on the panel or due to change of the input-group size. - this._sizeAttributeObserver.observe(input, { attributeFilter: ['size'] }); + this._sizeAttributeObserver?.observe(input, { attributeFilter: ['size'] }); this._updateState(); } diff --git a/src/elements/stepper/step/step.ts b/src/elements/stepper/step/step.ts index 6ec7b88428..b0cccc1a30 100644 --- a/src/elements/stepper/step/step.ts +++ b/src/elements/stepper/step/step.ts @@ -1,17 +1,16 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import { type CSSResultGroup, html, LitElement, - type TemplateResult, type PropertyValues, + type TemplateResult, } from 'lit'; import { customElement } from 'lit/decorators.js'; -import { ref } from 'lit/directives/ref.js'; import { SbbConnectedAbortController } from '../../core/controllers.js'; import { hostAttributes } from '../../core/decorators.js'; import { EventEmitter } from '../../core/eventing.js'; -import { AgnosticResizeObserver } from '../../core/observers.js'; import type { SbbStepLabelElement } from '../step-label.js'; import type { SbbStepperElement } from '../stepper.js'; @@ -53,9 +52,11 @@ export class SbbStepElement extends LitElement { private _abort = new SbbConnectedAbortController(this); private _stepper: SbbStepperElement | null = null; private _label: SbbStepLabelElement | null = null; - private _stepResizeObserver = new AgnosticResizeObserver((entries) => - this._onStepElementResize(entries), - ); + private _stepResizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: (entries) => this._onStepElementResize(entries), + }); /** The label of the step. */ public get label(): SbbStepLabelElement | null { @@ -155,20 +156,13 @@ export class SbbStepElement extends LitElement { protected override firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); this._loaded = true; - } - - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._stepResizeObserver.disconnect(); + this._stepResizeObserver.observe(this.shadowRoot!.querySelector('.sbb-step') as HTMLElement); } protected override render(): TemplateResult { return html`
-
step && this._stepResizeObserver.observe(step as HTMLElement))} - > +
diff --git a/src/elements/table/table-wrapper/table-wrapper.ts b/src/elements/table/table-wrapper/table-wrapper.ts index fb37857c9f..1b15589606 100644 --- a/src/elements/table/table-wrapper/table-wrapper.ts +++ b/src/elements/table/table-wrapper/table-wrapper.ts @@ -1,3 +1,4 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import { type CSSResultGroup, html, @@ -8,7 +9,6 @@ import { import { customElement } from 'lit/decorators.js'; import { SbbNegativeMixin } from '../../core/mixins.js'; -import { AgnosticResizeObserver } from '../../core/observers.js'; import style from './table-wrapper.scss?lit&inline'; @@ -21,14 +21,13 @@ import style from './table-wrapper.scss?lit&inline'; export class SbbTableWrapperElement extends SbbNegativeMixin(LitElement) { public static override styles: CSSResultGroup = style; - private _resizeObserver = new AgnosticResizeObserver(() => this._checkHorizontalScrollbar()); + private _resizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: () => this._checkHorizontalScrollbar(), + }); private _tableWrapper!: HTMLElement; - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._resizeObserver.disconnect(); - } - protected override firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); this._tableWrapper = this.shadowRoot!.querySelector('.sbb-table-wrapper')!; diff --git a/src/elements/tabs/tab-group/tab-group.ts b/src/elements/tabs/tab-group/tab-group.ts index 2cd7585efb..4bed4522f8 100644 --- a/src/elements/tabs/tab-group/tab-group.ts +++ b/src/elements/tabs/tab-group/tab-group.ts @@ -1,3 +1,5 @@ +import { MutationController } from '@lit-labs/observers/mutation-controller.js'; +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @@ -7,7 +9,6 @@ import { getNextElementIndex, isArrowKeyPressed } from '../../core/a11y.js'; import { SbbConnectedAbortController } from '../../core/controllers.js'; import { EventEmitter, throttle } from '../../core/eventing.js'; import { SbbHydrationMixin } from '../../core/mixins.js'; -import { AgnosticMutationObserver, AgnosticResizeObserver } from '../../core/observers.js'; import type { SbbTabLabelElement } from '../tab-label.js'; import { SbbTabElement } from '../tab.js'; @@ -64,15 +65,21 @@ export class SbbTabGroupElement extends SbbHydrationMixin(LitElement) { private _tabGroupElement!: HTMLElement; private _tabContentElement!: HTMLElement; private _abort = new SbbConnectedAbortController(this); - private _tabAttributeObserver = new AgnosticMutationObserver((mutationsList) => - this._onTabAttributesChange(mutationsList), - ); - private _tabGroupResizeObserver = new AgnosticResizeObserver((entries) => - this._onTabGroupElementResize(entries), - ); - private _tabContentResizeObserver = new AgnosticResizeObserver((entries) => - this._onTabContentElementResize(entries), - ); + private _tabAttributeObserver = new MutationController(this, { + target: null, + config: tabObserverConfig, + callback: (mutationsList) => this._onTabAttributesChange(mutationsList), + }); + private _tabGroupResizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: (entries) => this._onTabGroupElementResize(entries), + }); + private _tabContentResizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: (entries) => this._onTabContentElementResize(entries), + }); /** Size variant, either s, l or xl. */ @property() @@ -146,13 +153,6 @@ export class SbbTabGroupElement extends SbbHydrationMixin(LitElement) { return this._tabs.filter((t) => !t.hasAttribute('disabled')); } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._tabAttributeObserver.disconnect(); - this._tabContentResizeObserver.disconnect(); - this._tabGroupResizeObserver.disconnect(); - } - private _updateSize(): void { for (const tab of this._tabs) { tab.setAttribute('data-size', this.size); @@ -343,7 +343,7 @@ export class SbbTabGroupElement extends SbbHydrationMixin(LitElement) { tabLabel.tab.toggleAttribute('active', tabLabel.active); } - this._tabAttributeObserver.observe(tabLabel, tabObserverConfig); + this._tabAttributeObserver.observe(tabLabel); tabLabel.slot = 'tab-bar'; } diff --git a/src/elements/toggle/toggle/toggle.ts b/src/elements/toggle/toggle/toggle.ts index 31b93794af..d59fb5941c 100644 --- a/src/elements/toggle/toggle/toggle.ts +++ b/src/elements/toggle/toggle/toggle.ts @@ -1,3 +1,4 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import { type CSSResultGroup, html, @@ -12,7 +13,6 @@ import { getNextElementIndex, interactivityChecker, isArrowKeyPressed } from '.. import { SbbConnectedAbortController } from '../../core/controllers.js'; import { hostAttributes } from '../../core/decorators.js'; import { EventEmitter } from '../../core/eventing.js'; -import { AgnosticResizeObserver } from '../../core/observers.js'; import type { SbbToggleOptionElement } from '../toggle-option.js'; import style from './toggle.scss?lit&inline'; @@ -80,7 +80,11 @@ export class SbbToggleElement extends LitElement { } private _loaded: boolean = false; - private _toggleResizeObserver = new AgnosticResizeObserver(() => this.updatePillPosition(true)); + private _toggleResizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: () => this.updatePillPosition(true), + }); /** * @deprecated only used for React. Will probably be removed once React 19 is available. @@ -146,11 +150,6 @@ export class SbbToggleElement extends LitElement { this.updatePillPosition(false); } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._toggleResizeObserver.disconnect(); - } - private _updateToggle(): void { this._valueChanged(this.value); this._updateDisabled(); diff --git a/src/elements/train/train-formation/train-formation.ts b/src/elements/train/train-formation/train-formation.ts index 3a086ef31f..f6281246ee 100644 --- a/src/elements/train/train-formation/train-formation.ts +++ b/src/elements/train/train-formation/train-formation.ts @@ -1,3 +1,4 @@ +import { ResizeController } from '@lit-labs/observers/resize-controller.js'; import { type CSSResultGroup, html, @@ -12,7 +13,6 @@ import { ref } from 'lit/directives/ref.js'; import { SbbConnectedAbortController, SbbLanguageController } from '../../core/controllers.js'; import { i18nSector, i18nSectorShort, i18nTrains } from '../../core/i18n.js'; import { SbbNamedSlotListMixin, type WithListChildren } from '../../core/mixins.js'; -import { AgnosticResizeObserver } from '../../core/observers.js'; import type { SbbTrainBlockedPassageElement } from '../train-blocked-passage.js'; import type { SbbTrainWagonElement } from '../train-wagon.js'; import type { SbbTrainElement } from '../train.js'; @@ -45,8 +45,12 @@ export class SbbTrainFormationElement extends SbbNamedSlotListMixin< @state() private _sectors: AggregatedSector[] = []; /** Element that defines the visible content width. */ - private _formationDiv!: HTMLDivElement; - private _contentResizeObserver = new AgnosticResizeObserver(() => this._applyCssWidth()); + private _formationDiv?: HTMLDivElement; + private _contentResizeObserver = new ResizeController(this, { + target: null, + skipInitial: true, + callback: () => this._applyCssWidth(), + }); private _abort = new SbbConnectedAbortController(this); private _language = new SbbLanguageController(this); @@ -57,18 +61,13 @@ export class SbbTrainFormationElement extends SbbNamedSlotListMixin< this.addEventListener('sectorChange', (e) => this._readSectors(e), { signal }); } - public override disconnectedCallback(): void { - super.disconnectedCallback(); - this._contentResizeObserver.disconnect(); - } - /** * Apply width of the scrollable space of the formation as a css variable. This will be used from * every slotted sbb-train for the direction-label */ private _applyCssWidth(): void { - const contentWidth = this._formationDiv.getBoundingClientRect().width; - this._formationDiv.style.setProperty('--sbb-train-direction-width', `${contentWidth}px`); + const contentWidth = this._formationDiv!.getBoundingClientRect().width; + this._formationDiv!.style.setProperty('--sbb-train-direction-width', `${contentWidth}px`); } private _readSectors(event?: Event): void { @@ -117,7 +116,9 @@ export class SbbTrainFormationElement extends SbbNamedSlotListMixin< if (!el) { return; } - this._contentResizeObserver.disconnect(); + if (this._formationDiv) { + this._contentResizeObserver.unobserve(this._formationDiv); + } this._formationDiv = el as HTMLDivElement; this._contentResizeObserver.observe(this._formationDiv); // There seems to be a slight difference between browser, in how the diff --git a/tools/vite/package-json-template.ts b/tools/vite/package-json-template.ts index 56327d96b1..00cfb64373 100644 --- a/tools/vite/package-json-template.ts +++ b/tools/vite/package-json-template.ts @@ -27,6 +27,8 @@ export function packageJsonTemplate( const packageJsonTemplatePath = options.templatePath ?? './package.json'; const rootPackageJson = JSON.parse(readFileSync(new URL(rootPackageJsonPath, root), 'utf8')); const litMajorVersion = +rootPackageJson.dependencies.lit.match(/\d+/); + const litObserversMajorVersion = + +rootPackageJson.devDependencies['@lit-labs/observers'].match(/\d+/); const reactMajorVersion = +rootPackageJson.devDependencies.react.match(/\d+/); const litReactMajorVersion = +rootPackageJson.devDependencies['@lit/react'].match(/\d+/); const packageJsonTemplate = readFileSync( @@ -37,7 +39,8 @@ export function packageJsonTemplate( .replaceAll('0.0.0-PLACEHOLDER', rootPackageJson.version) .replaceAll('0.0.0-LITREACT', `^${litReactMajorVersion}.0.0`) .replaceAll('0.0.0-REACT', `^${reactMajorVersion}.0.0`) - .replaceAll('0.0.0-LIT', `^${litMajorVersion}.0.0`); + .replaceAll('0.0.0-LIT', `^${litMajorVersion}.0.0`) + .replaceAll('0.0.0-LITOBSERVERS', `^${litObserversMajorVersion}.0.0`); const packageJson = JSON.parse(packageJsonContent); for (const key of ['author', 'license', 'repository', 'bugs']) { packageJson[key] = rootPackageJson[key]; diff --git a/yarn.lock b/yarn.lock index ef1178c3ea..52de44ad43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -615,6 +615,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@lit-labs/observers@2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@lit-labs/observers/-/observers-2.0.3.tgz#a42a72cfa288dbd3065c7edb50313ad8d3ea6fe1" + integrity sha512-CeftEJ2TId9iohDJHLjUXiSBVndqjIBaALjeTt8OmgWLh2dnIzwlj4WtPCiJw15uR1s6D6wyCsw0AoJC5/9QXw== + dependencies: + "@lit/reactive-element" "^1.0.0 || ^2.0.0" + lit-html "^3.2.0" + "@lit-labs/router@0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@lit-labs/router/-/router-0.1.3.tgz#6be268eec6bbcbf0d28ee66688440cc46f587882" @@ -669,7 +677,7 @@ resolved "https://registry.yarnpkg.com/@lit/react/-/react-1.0.5.tgz#9c53a8d719f91ef7edca0bdd68f5589ea579ffc1" integrity sha512-RSHhrcuSMa4vzhqiTenzXvtQ6QDq3hSPsnHHO3jaPmmvVFeoNNm4DHoQ0zLdKAUvY3wP3tTENSUf7xpyVfrDEA== -"@lit/reactive-element@^2.0.4": +"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b" integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==