diff --git a/src/elements/autocomplete/autocomplete-base-element.ts b/src/elements/autocomplete/autocomplete-base-element.ts index 07f456bb37..7612dac1c2 100644 --- a/src/elements/autocomplete/autocomplete-base-element.ts +++ b/src/elements/autocomplete/autocomplete-base-element.ts @@ -357,6 +357,9 @@ export abstract class SbbAutocompleteBaseElement extends SbbNegativeMixin( document.addEventListener('scroll', () => this._setOverlayPosition(), { passive: true, signal: this._openPanelEventsController.signal, + // Without capture, other scroll contexts would not bubble to this event listener. + // Capture allows us to react to all scroll contexts in this DOM. + capture: true, }); window.addEventListener('resize', () => this._setOverlayPosition(), { passive: true, diff --git a/src/elements/core/eventing/forward-event.ts b/src/elements/core/eventing/forward-event.ts index 0ced563183..54ba802c64 100644 --- a/src/elements/core/eventing/forward-event.ts +++ b/src/elements/core/eventing/forward-event.ts @@ -1,9 +1,16 @@ /** - * Forwards an event to the host element provided. - * This way, an event triggered in the ShadowDOM can cross its boundary and can be listened on the host component. + * Forwards an event to the element provided. + * This way, an event triggered in the Shadow DOM can cross its boundary and can be listened on e.g. the host component. */ -export function forwardEventToHost(event: Event, host: HTMLElement): void { +export function forwardEvent(event: Event, element: HTMLElement | Document): void { const eventConstructor = Object.getPrototypeOf(event).constructor; const copiedEvent: Event = new eventConstructor(event.type, event); - host.dispatchEvent(copiedEvent); + element.dispatchEvent(copiedEvent); } + +/** + * Forwards an event to the host element provided. + * This way, an event triggered in the ShadowDOM can cross its boundary and can be listened on the host component. + * @deprecated will be removed with next major version, use forwardEvent as alternative + */ +export const forwardEventToHost = forwardEvent; diff --git a/src/elements/datepicker/datepicker/datepicker.ts b/src/elements/datepicker/datepicker/datepicker.ts index 1e23d0c3dc..3010af67a3 100644 --- a/src/elements/datepicker/datepicker/datepicker.ts +++ b/src/elements/datepicker/datepicker/datepicker.ts @@ -13,7 +13,7 @@ import { SbbConnectedAbortController, SbbLanguageController } from '../../core/c import { type DateAdapter, defaultDateAdapter } from '../../core/datetime.js'; import { forceType } from '../../core/decorators.js'; import { findInput, findReferencedElement } from '../../core/dom.js'; -import { EventEmitter, forwardEventToHost } from '../../core/eventing.js'; +import { EventEmitter, forwardEvent } from '../../core/eventing.js'; import { i18nDateChangedTo, i18nDatePickerPlaceholder } from '../../core/i18n.js'; import type { SbbDateLike, SbbValidationChangeEvent } from '../../core/interfaces.js'; import type { SbbDatepickerButton } from '../common.js'; @@ -243,7 +243,7 @@ class SbbDatepickerElement extends LitElement { input.addEventListener( 'input', (e) => { - forwardEventToHost(e, this); + forwardEvent(e, this); this._parseInput(); }, options, diff --git a/src/elements/dialog/dialog-content/dialog-content.ts b/src/elements/dialog/dialog-content/dialog-content.ts index ddb67269cb..d724d3a457 100644 --- a/src/elements/dialog/dialog-content/dialog-content.ts +++ b/src/elements/dialog/dialog-content/dialog-content.ts @@ -2,6 +2,8 @@ import type { CSSResultGroup, TemplateResult } from 'lit'; import { html, LitElement } from 'lit'; import { customElement } from 'lit/decorators.js'; +import { forwardEvent } from '../../core/eventing.js'; + import style from './dialog-content.scss?lit&inline'; /** @@ -16,7 +18,7 @@ class SbbDialogContentElement extends LitElement { protected override render(): TemplateResult { return html` -
+
forwardEvent(e, document)}>
`; diff --git a/src/elements/dialog/dialog/dialog.spec.ts b/src/elements/dialog/dialog/dialog.spec.ts index ddfcb1f04a..19e59d1efc 100644 --- a/src/elements/dialog/dialog/dialog.spec.ts +++ b/src/elements/dialog/dialog/dialog.spec.ts @@ -472,18 +472,21 @@ describe('sbb-dialog with long content', () => { it('shows/hides the dialog header on scroll', async () => { await openDialog(element); expect(element).not.to.have.attribute('data-hide-header'); + const scrollSpy = new EventSpy('scroll', document); const content = element.querySelector('sbb-dialog-content')!.shadowRoot!.firstElementChild!; // Scroll down. content.scrollTo(0, 50); await waitForCondition(() => element.hasAttribute('data-hide-header')); + await scrollSpy.calledOnce(); expect(element).to.have.attribute('data-hide-header'); // Scroll up. content.scrollTo(0, 0); await waitForCondition(() => !element.hasAttribute('data-hide-header')); + await scrollSpy.calledTimes(2); expect(element).not.to.have.attribute('data-hide-header'); }); diff --git a/src/elements/dialog/dialog/dialog.ts b/src/elements/dialog/dialog/dialog.ts index a84dd0a41e..ed36e9a3bb 100644 --- a/src/elements/dialog/dialog/dialog.ts +++ b/src/elements/dialog/dialog/dialog.ts @@ -120,7 +120,6 @@ class SbbDialogElement extends SbbOverlayBaseElement { private _handleOpening(): void { this.state = 'opened'; - this.didOpen.emit(); this.inertController.activate(); this.attachOpenOverlayEvents(); this.setOverlayFocus(); @@ -131,6 +130,7 @@ class SbbDialogElement extends SbbOverlayBaseElement { ), ); this.focusHandler.trap(this); + this.didOpen.emit(); } public override connectedCallback(): void { diff --git a/src/elements/file-selector/common/file-selector-common.ts b/src/elements/file-selector/common/file-selector-common.ts index 835dd4f68c..0451e0beb8 100644 --- a/src/elements/file-selector/common/file-selector-common.ts +++ b/src/elements/file-selector/common/file-selector-common.ts @@ -9,7 +9,7 @@ import { sbbInputModalityDetector } from '../../core/a11y.js'; import { SbbLanguageController } from '../../core/controllers.js'; import { forceType } from '../../core/decorators.js'; import { isLean } from '../../core/dom.js'; -import { EventEmitter, forwardEventToHost } from '../../core/eventing.js'; +import { EventEmitter, forwardEvent } from '../../core/eventing.js'; import { i18nFileSelectorButtonLabel, i18nFileSelectorCurrentlySelected, @@ -183,7 +183,7 @@ export const SbbFileSelectorCommonElementMixin = { let element: SbbMapContainerElement; it('should react to scrolling', async () => { - await setViewport({ width: 320, height: 600 }); - element = await fixture( - html` + html`
Operations & Disruptions

Situation 1

@@ -34,6 +32,9 @@ describe(`sbb-map-container`, () => {
`, ); + + await setViewport({ width: 320, height: 600 }); + assert.instanceOf(element, SbbMapContainerElement); function getInert(): boolean { @@ -52,4 +53,35 @@ describe(`sbb-map-container`, () => { expect(element).to.have.attribute('data-scroll-up-button-visible'); expect(getInert()).to.be.equal(false); }); + + it('should forward scroll event in sidebar on bigger viewports', async () => { + element = await fixture( + html` +
+ Operations & Disruptions +

Situation 1

+

Situation 2

+

Situation 3

+

Situation 4

+

Situation 5

+

Situation 6

+

Situation 7

+

Situation 8

+
+
+
map
+
+
`, + ); + + await setViewport({ width: 1000, height: 300 }); + + const scrollSpy = new EventSpy('scroll', document); + const scrollContext = element.shadowRoot!.querySelector('.sbb-map-container__sidebar')!; + + scrollContext.scrollTo(0, 400); + + await scrollSpy.calledOnce(); + expect(scrollSpy.count).to.be.equal(1); + }); }); diff --git a/src/elements/map-container/map-container.ts b/src/elements/map-container/map-container.ts index 16aa28f91b..620233ebcd 100644 --- a/src/elements/map-container/map-container.ts +++ b/src/elements/map-container/map-container.ts @@ -5,6 +5,7 @@ import { customElement, property, state } from 'lit/decorators.js'; import { SbbLanguageController } from '../core/controllers.js'; import { forceType } from '../core/decorators.js'; +import { forwardEvent } from '../core/eventing.js'; import { i18nMapContainerButtonLabel } from '../core/i18n.js'; import style from './map-container.scss?lit&inline'; @@ -96,7 +97,7 @@ class SbbMapContainerElement extends LitElement {
-
+
forwardEvent(e, document)}> diff --git a/src/elements/menu/menu/menu.spec.ts b/src/elements/menu/menu/menu.spec.ts index bb40ed12a7..02318af7fb 100644 --- a/src/elements/menu/menu/menu.spec.ts +++ b/src/elements/menu/menu/menu.spec.ts @@ -52,11 +52,9 @@ describe(`sbb-menu`, () => { await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); }); @@ -71,12 +69,9 @@ describe(`sbb-menu`, () => { await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); await sendKeys({ press: tabKey }); @@ -87,11 +82,9 @@ describe(`sbb-menu`, () => { await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'closed'); }); @@ -108,11 +101,9 @@ describe(`sbb-menu`, () => { await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); expect(menuAction).not.to.be.null; @@ -122,11 +113,8 @@ describe(`sbb-menu`, () => { await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'closed'); }); @@ -142,11 +130,9 @@ describe(`sbb-menu`, () => { await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); expect(menuLink).not.to.be.null; @@ -156,11 +142,9 @@ describe(`sbb-menu`, () => { await willCloseEventSpy.calledOnce(); expect(willCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'closed'); }); @@ -201,11 +185,9 @@ describe(`sbb-menu`, () => { await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); @@ -236,11 +218,9 @@ describe(`sbb-menu`, () => { await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'opened'); @@ -262,15 +242,11 @@ describe(`sbb-menu`, () => { await willOpenEventSpy.calledOnce(); expect(willOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); - await waitForLitRender(element); expect(document.activeElement!.id).to.be.equal('menu-link'); }); @@ -292,15 +268,33 @@ describe(`sbb-menu`, () => { const willCloseEventSpy = new EventSpy(SbbMenuElement.events.willClose, element); element.open(); - await didOpenEventSpy.calledOnce(); await waitForLitRender(element); + await didOpenEventSpy.calledOnce(); element.addEventListener(SbbMenuElement.events.willClose, (ev) => ev.preventDefault()); element.close(); - await willCloseEventSpy.calledOnce(); await waitForLitRender(element); + await willCloseEventSpy.calledOnce(); expect(element).to.have.attribute('data-state', 'opened'); }); + + it('does forward scroll event to document', async () => { + const didOpenEventSpy = new EventSpy(SbbMenuElement.events.didOpen, element); + + element.open(); + await waitForLitRender(element); + await didOpenEventSpy.calledOnce(); + + await setViewport({ width: 320, height: 300 }); + + const scrollSpy = new EventSpy('scroll', document); + const scrollContext = element.shadowRoot!.querySelector('.sbb-menu__content')!; + + scrollContext.scrollTo(0, 400); + + await scrollSpy.calledOnce(); + expect(scrollSpy.count).to.be.equal(1); + }); }); diff --git a/src/elements/menu/menu/menu.ts b/src/elements/menu/menu/menu.ts index 724ac8e640..eedbc67d60 100644 --- a/src/elements/menu/menu/menu.ts +++ b/src/elements/menu/menu/menu.ts @@ -23,6 +23,7 @@ import { isZeroAnimationDuration, SbbScrollHandler, } from '../../core/dom.js'; +import { forwardEvent } from '../../core/eventing.js'; import { SbbNamedSlotListMixin } from '../../core/mixins.js'; import { getElementPosition, @@ -168,11 +169,11 @@ class SbbMenuElement extends SbbNamedSlotListMixin< private _handleOpening(): void { this.state = 'opened'; - this.didOpen.emit(); this._inertController.activate(); this._setMenuFocus(); this._focusHandler.trap(this); this._attachWindowEvents(); + this.didOpen.emit(); } private _handleClosing(): void { @@ -319,6 +320,9 @@ class SbbMenuElement extends SbbNamedSlotListMixin< document.addEventListener('scroll', () => this._setMenuPosition(), { passive: true, signal: this._windowEventsController.signal, + // Without capture, other scroll contexts would not bubble to this event listener. + // Capture allows us to react to all scroll contexts in this DOM. + capture: true, }); window.addEventListener('resize', () => this._setMenuPosition(), { passive: true, @@ -413,6 +417,7 @@ class SbbMenuElement extends SbbNamedSlotListMixin< >
this._closeOnInteractiveElementClick(event)} + @scroll=${(e: Event) => forwardEvent(e, document)} class="sbb-menu__content" > ${this.listChildren.length diff --git a/src/elements/navigation/navigation/navigation.spec.ts b/src/elements/navigation/navigation/navigation.spec.ts index b624c52bb3..8813ddd289 100644 --- a/src/elements/navigation/navigation/navigation.spec.ts +++ b/src/elements/navigation/navigation/navigation.spec.ts @@ -89,12 +89,7 @@ describe(`sbb-navigation`, () => { await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); - - await waitForLitRender(element); - expect(action2).to.have.attribute('data-action-active'); expect(action3).to.have.attribute('data-action-active'); expect(element.shadowRoot?.activeElement?.id).to.be.equal('sbb-navigation-close-button'); @@ -135,12 +130,7 @@ describe(`sbb-navigation`, () => { await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); - - await waitForLitRender(element); - expect(actionActive).to.have.attribute('data-action-active'); expect(sectionActionActive).to.have.attribute('data-action-active'); expect(activeSection).to.have.attribute('data-state', 'opened'); @@ -177,12 +167,7 @@ describe(`sbb-navigation`, () => { await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); - - await waitForLitRender(element); - expect(action2).to.have.attribute('data-action-active'); expect(action3).to.have.attribute('data-action-active'); @@ -202,8 +187,6 @@ describe(`sbb-navigation`, () => { await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'closed'); element.open(); @@ -211,12 +194,7 @@ describe(`sbb-navigation`, () => { await didOpenEventSpy.calledTimes(2); expect(didOpenEventSpy.count).to.be.equal(2); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); - - await waitForLitRender(element); - expect(action1).not.to.have.attribute('data-action-active'); expect(action4).not.to.have.attribute('data-action-active'); }); @@ -230,8 +208,6 @@ describe(`sbb-navigation`, () => { await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); element.close(); @@ -239,8 +215,6 @@ describe(`sbb-navigation`, () => { await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'closed'); }); @@ -255,8 +229,6 @@ describe(`sbb-navigation`, () => { await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); closeButton.click(); @@ -264,8 +236,6 @@ describe(`sbb-navigation`, () => { await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'closed'); }); @@ -290,8 +260,6 @@ describe(`sbb-navigation`, () => { await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'closed'); }); @@ -324,8 +292,6 @@ describe(`sbb-navigation`, () => { await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'closed'); expect(section).to.have.attribute('data-state', 'closed'); }); @@ -341,8 +307,6 @@ describe(`sbb-navigation`, () => { await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); element.close(); @@ -350,8 +314,6 @@ describe(`sbb-navigation`, () => { await didCloseEventSpy.calledOnce(); expect(didCloseEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'closed'); }); @@ -367,8 +329,6 @@ describe(`sbb-navigation`, () => { await didOpenEventSpy.calledOnce(); expect(didOpenEventSpy.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); expect(section).to.have.attribute('data-state', 'closed'); diff --git a/src/elements/navigation/navigation/navigation.ts b/src/elements/navigation/navigation/navigation.ts index 4c273f2002..182e0c60bc 100644 --- a/src/elements/navigation/navigation/navigation.ts +++ b/src/elements/navigation/navigation/navigation.ts @@ -206,13 +206,13 @@ class SbbNavigationElement extends SbbUpdateSchedulerMixin(SbbOpenCloseBaseEleme private _handleOpening(): void { this.state = 'opened'; - this.didOpen.emit(); this._navigationResizeObserver.observe(this); this._inertController.activate(); this._focusHandler.trap(this, { filter: this._trapFocusFilter }); this._attachWindowEvents(); this._setNavigationFocus(); this.completeUpdate(); + this.didOpen.emit(); } // Removes trigger click listener on trigger change. diff --git a/src/elements/notification/notification.ts b/src/elements/notification/notification.ts index 90f2ca6a73..b474c9a4ac 100644 --- a/src/elements/notification/notification.ts +++ b/src/elements/notification/notification.ts @@ -219,8 +219,8 @@ class SbbNotificationElement extends LitElement { private _handleOpening(): void { this._state = 'opened'; - this._didOpen.emit(); this._notificationResizeObserver.observe(this._notificationElement); + this._didOpen.emit(); } private _handleClosing(): void { diff --git a/src/elements/overlay/overlay.spec.ts b/src/elements/overlay/overlay.spec.ts index bf5f6b56f1..1055e4cf48 100644 --- a/src/elements/overlay/overlay.spec.ts +++ b/src/elements/overlay/overlay.spec.ts @@ -20,12 +20,9 @@ async function openOverlay(element: SbbOverlayElement): Promise { await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); - await waitForLitRender(element); await didOpen.calledOnce(); expect(didOpen.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'opened'); } @@ -37,6 +34,9 @@ describe('sbb-overlay', () => { element = await fixture(html`

Overlay content

+

Overlay content

+

Overlay content

+

Overlay content

`); ariaLiveRef = element.shadowRoot!.querySelector('sbb-screen-reader-only')!; @@ -61,8 +61,6 @@ describe('sbb-overlay', () => { await willOpen.calledOnce(); expect(willOpen.count).to.be.equal(1); - await waitForLitRender(element); - expect(didOpen.count).to.be.equal(0); expect(element).to.have.attribute('data-state', 'closed'); }); @@ -80,12 +78,9 @@ describe('sbb-overlay', () => { await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); - await waitForLitRender(element); await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'closed'); expect(ariaLiveRef.textContent).to.be.equal(''); }); @@ -101,8 +96,6 @@ describe('sbb-overlay', () => { await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); - await waitForLitRender(element); - expect(element).to.have.attribute('data-state', 'closed'); }); @@ -119,8 +112,6 @@ describe('sbb-overlay', () => { await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); - await waitForLitRender(element); - expect(didClose.count).to.be.equal(0); expect(element).to.have.attribute('data-state', 'opened'); }); @@ -137,11 +128,9 @@ describe('sbb-overlay', () => { await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); - await waitForLitRender(element); await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'closed'); }); @@ -186,11 +175,9 @@ describe('sbb-overlay', () => { await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); - await waitForLitRender(element); await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); - await waitForLitRender(element); expect(element).to.have.attribute('data-state', 'closed'); }); @@ -220,11 +207,9 @@ describe('sbb-overlay', () => { await willOpen.calledTimes(2); expect(willOpen.count).to.be.equal(2); - await waitForLitRender(element); await didOpen.calledTimes(2); expect(didOpen.count).to.be.equal(2); - await waitForLitRender(element); expect(stackedOverlay).to.have.attribute('data-state', 'opened'); @@ -236,11 +221,9 @@ describe('sbb-overlay', () => { await willClose.calledOnce(); expect(willClose.count).to.be.equal(1); - await waitForLitRender(element); await didClose.calledOnce(); expect(didClose.count).to.be.equal(1); - await waitForLitRender(element); expect(stackedOverlay).to.have.attribute('data-state', 'closed'); expect(element).to.have.attribute('data-state', 'opened'); @@ -253,11 +236,9 @@ describe('sbb-overlay', () => { await willClose.calledTimes(2); expect(willClose.count).to.be.equal(2); - await waitForLitRender(element); await didClose.calledTimes(2); expect(didClose.count).to.be.equal(2); - await waitForLitRender(element); expect(stackedOverlay).to.have.attribute('data-state', 'closed'); expect(element).to.have.attribute('data-state', 'closed'); @@ -296,4 +277,22 @@ describe('sbb-overlay', () => { expect(ariaLiveRef.textContent!.trim()).to.be.equal(`${i18nDialog.en}, Special Overlay`); }); + + it('does forward scroll event to document', async () => { + const didOpenEventSpy = new EventSpy(SbbOverlayElement.events.didOpen, element); + + element.open(); + await waitForLitRender(element); + await didOpenEventSpy.calledOnce(); + + await setViewport({ width: 320, height: 300 }); + + const scrollSpy = new EventSpy('scroll', document); + const scrollContext = element.shadowRoot!.querySelector('.sbb-overlay__content')!; + + scrollContext.scrollTo(0, 400); + + await scrollSpy.calledOnce(); + expect(scrollSpy.count).to.be.equal(1); + }); }); diff --git a/src/elements/overlay/overlay.ts b/src/elements/overlay/overlay.ts index 5d729b79a8..008fc6df64 100644 --- a/src/elements/overlay/overlay.ts +++ b/src/elements/overlay/overlay.ts @@ -6,7 +6,7 @@ import { html, unsafeStatic } from 'lit/static-html.js'; import { getFirstFocusableElement, setModalityOnNextFocus } from '../core/a11y.js'; import { forceType } from '../core/decorators.js'; import { isZeroAnimationDuration } from '../core/dom.js'; -import { EventEmitter } from '../core/eventing.js'; +import { EventEmitter, forwardEvent } from '../core/eventing.js'; import { i18nCloseDialog, i18nGoBack } from '../core/i18n.js'; import { overlayRefs, SbbOverlayBaseElement } from './overlay-base-element.js'; @@ -111,13 +111,13 @@ class SbbOverlayElement extends SbbOverlayBaseElement { private _handleOpening(): void { this.state = 'opened'; - this.didOpen.emit(); this.inertController.activate(); this.attachOpenOverlayEvents(); this.setOverlayFocus(); // Use timeout to read label after focused element setTimeout(() => this.setAriaLiveRefContent(this.accessibilityLabel)); this.focusHandler.trap(this); + this.didOpen.emit(); } protected override handleClosing(): void { @@ -201,7 +201,7 @@ class SbbOverlayElement extends SbbOverlayBaseElement {
${this.backButton ? backButton : nothing} ${closeButton}
-
+
forwardEvent(e, document)}> el !== this._overlay, }); + this.didOpen.emit(); } // Closes the popover on "Esc" key pressed and traps focus within the popover. @@ -317,6 +317,9 @@ class SbbPopoverElement extends SbbHydrationMixin(SbbOpenCloseBaseElement) { document.addEventListener('scroll', () => this._setPopoverPosition(), { passive: true, signal: this._openStateController.signal, + // Without capture, other scroll contexts would not bubble to this event listener. + // Capture allows us to react to all scroll contexts in this DOM. + capture: true, }); window.addEventListener('resize', () => this._setPopoverPosition(), { passive: true, diff --git a/src/elements/select/select.ts b/src/elements/select/select.ts index df39866159..e485583230 100644 --- a/src/elements/select/select.ts +++ b/src/elements/select/select.ts @@ -577,6 +577,9 @@ class SbbSelectElement extends SbbUpdateSchedulerMixin( document.addEventListener('scroll', () => this._setOverlayPosition(), { passive: true, signal: this._openPanelEventsController.signal, + // Without capture, other scroll contexts would not bubble to this event listener. + // Capture allows us to react to all scroll contexts in this DOM. + capture: true, }); window.addEventListener('resize', () => this._setOverlayPosition(), { passive: true, diff --git a/src/elements/slider/slider.ts b/src/elements/slider/slider.ts index cd13d25753..0bc5e39b60 100644 --- a/src/elements/slider/slider.ts +++ b/src/elements/slider/slider.ts @@ -6,7 +6,7 @@ import { styleMap } from 'lit/directives/style-map.js'; import { SbbConnectedAbortController } from '../core/controllers.js'; import { forceType, hostAttributes } from '../core/decorators.js'; -import { EventEmitter, forwardEventToHost } from '../core/eventing.js'; +import { EventEmitter, forwardEvent } from '../core/eventing.js'; import { type FormRestoreReason, type FormRestoreState, @@ -252,7 +252,7 @@ class SbbSliderElement extends SbbDisabledMixin(SbbFormAssociatedMixin(LitElemen /** Emits the change event. */ private _emitChange(event: Event): void { - forwardEventToHost(event, this); + forwardEvent(event, this); this._didChange.emit(); } diff --git a/src/elements/time-input/time-input.ts b/src/elements/time-input/time-input.ts index cc8e0fec38..ba02e5787a 100644 --- a/src/elements/time-input/time-input.ts +++ b/src/elements/time-input/time-input.ts @@ -5,7 +5,7 @@ import { ref } from 'lit/directives/ref.js'; import { SbbLanguageController } from '../core/controllers.js'; import { findInput } from '../core/dom.js'; -import { EventEmitter, forwardEventToHost } from '../core/eventing.js'; +import { EventEmitter, forwardEvent } from '../core/eventing.js'; import { i18nTimeInputChange } from '../core/i18n.js'; import type { SbbDateLike, SbbValidationChangeEvent } from '../core/interfaces.js'; @@ -132,11 +132,9 @@ class SbbTimeInputElement extends LitElement { this._inputElement.placeholder = 'HH:MM'; } - this._inputElement.addEventListener( - 'input', - (event: Event) => forwardEventToHost(event, this), - { signal: this._abortController.signal }, - ); + this._inputElement.addEventListener('input', (event: Event) => forwardEvent(event, this), { + signal: this._abortController.signal, + }); this._inputElement.addEventListener( 'keydown', (event: KeyboardEvent) => this._preventCharInsert(event), @@ -196,7 +194,7 @@ class SbbTimeInputElement extends LitElement { /** Emits the change event. */ private _emitChange(event: Event): void { - forwardEventToHost(event, this); + forwardEvent(event, this); this._didChange.emit(); }