From 2d52b8e037dda8712eb63fe14fb7e19a96bdf347 Mon Sep 17 00:00:00 2001 From: Jeremias Peier Date: Wed, 18 Dec 2024 09:59:19 +0100 Subject: [PATCH] fix: handle scroll events in custom scroll contexts --- .../autocomplete/autocomplete-base-element.ts | 1 + .../autocomplete/autocomplete.visual.spec.ts | 40 ++++++++++++++ src/elements/core/eventing/forward-event.ts | 2 +- .../dialog/dialog-content/dialog-content.ts | 4 +- src/elements/dialog/dialog/dialog.stories.ts | 26 +++++++++ src/elements/dialog/dialog/dialog.ts | 2 +- .../dialog/dialog/dialog.visual.spec.ts | 54 +++++++++++++++++- src/elements/menu/menu/menu.spec.ts | 28 +--------- src/elements/menu/menu/menu.ts | 3 +- src/elements/menu/menu/menu.visual.spec.ts | 40 ++++++++++++++ .../navigation/navigation/navigation.spec.ts | 40 -------------- .../navigation/navigation/navigation.ts | 2 +- src/elements/notification/notification.ts | 2 +- src/elements/overlay/overlay.spec.ts | 22 -------- src/elements/overlay/overlay.ts | 9 ++- src/elements/overlay/overlay.visual.spec.ts | 55 ++++++++++++++++++- src/elements/popover/popover/popover.ts | 3 +- src/elements/select/select.ts | 1 + src/elements/select/select.visual.spec.ts | 39 +++++++++++++ 19 files changed, 273 insertions(+), 100 deletions(-) diff --git a/src/elements/autocomplete/autocomplete-base-element.ts b/src/elements/autocomplete/autocomplete-base-element.ts index 07f456bb37..5b4a685f52 100644 --- a/src/elements/autocomplete/autocomplete-base-element.ts +++ b/src/elements/autocomplete/autocomplete-base-element.ts @@ -357,6 +357,7 @@ export abstract class SbbAutocompleteBaseElement extends SbbNegativeMixin( document.addEventListener('scroll', () => this._setOverlayPosition(), { passive: true, signal: this._openPanelEventsController.signal, + capture: true, }); window.addEventListener('resize', () => this._setOverlayPosition(), { passive: true, diff --git a/src/elements/autocomplete/autocomplete.visual.spec.ts b/src/elements/autocomplete/autocomplete.visual.spec.ts index 6929965949..df4d5c2746 100644 --- a/src/elements/autocomplete/autocomplete.visual.spec.ts +++ b/src/elements/autocomplete/autocomplete.visual.spec.ts @@ -1,3 +1,4 @@ +import { aTimeout } from '@open-wc/testing'; import { sendKeys } from '@web/test-runner-commands'; import { html, nothing, type TemplateResult } from 'lit'; import { styleMap } from 'lit/directives/style-map.js'; @@ -289,5 +290,44 @@ describe('sbb-autocomplete', () => { }); }); } + + it( + `with scroll context`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` +
+

Content

+

Content

+

Content

+

Content

+ + + + Option 1 + Option 2 + + +

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+
+ `); + const scrollContext = setup.snapshotElement.querySelector('div')!; + const input = setup.snapshotElement.querySelector('input')!; + + setup.withSnapshotElement(scrollContext); + + setup.withPostSetupAction(async () => { + input.focus(); + scrollContext.scrollTo(0, 100); + + // We need to ensure content resizing kicked in + await aTimeout(40); + }); + }), + ); }); }); diff --git a/src/elements/core/eventing/forward-event.ts b/src/elements/core/eventing/forward-event.ts index 0ced563183..e6bfc3496b 100644 --- a/src/elements/core/eventing/forward-event.ts +++ b/src/elements/core/eventing/forward-event.ts @@ -2,7 +2,7 @@ * 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. */ -export function forwardEventToHost(event: Event, host: HTMLElement): void { +export function forwardEventToHost(event: Event, host: HTMLElement | Document): void { const eventConstructor = Object.getPrototypeOf(event).constructor; const copiedEvent: Event = new eventConstructor(event.type, event); host.dispatchEvent(copiedEvent); diff --git a/src/elements/dialog/dialog-content/dialog-content.ts b/src/elements/dialog/dialog-content/dialog-content.ts index ddb67269cb..a1eae203fe 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 { forwardEventToHost } 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` -
+
forwardEventToHost(e, document)}>
`; diff --git a/src/elements/dialog/dialog/dialog.stories.ts b/src/elements/dialog/dialog/dialog.stories.ts index 4c265ebe58..969fd5531e 100644 --- a/src/elements/dialog/dialog/dialog.stories.ts +++ b/src/elements/dialog/dialog/dialog.stories.ts @@ -17,6 +17,8 @@ import readme from './readme.md?raw'; import '../../button.js'; import '../../link.js'; import '../../form-field.js'; +import '../../select.js'; +import '../../option.js'; import '../../image.js'; import '../../popover.js'; import '../dialog-content.js'; @@ -225,6 +227,30 @@ const DefaultTemplate = ({ Dialog content

+ + + Option 1 + + Option 2 + + Option 3 + + Option 4 + + Option 5 + + +

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+

Content

Some content.

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/dialog/dialog/dialog.visual.spec.ts b/src/elements/dialog/dialog/dialog.visual.spec.ts index 1128c2bbf1..1fb8790f2e 100644 --- a/src/elements/dialog/dialog/dialog.visual.spec.ts +++ b/src/elements/dialog/dialog/dialog.visual.spec.ts @@ -1,3 +1,4 @@ +import { aTimeout } from '@open-wc/testing'; import { html, nothing, type TemplateResult } from 'lit'; import { describeViewports, visualDiffDefault } from '../../core/testing/private.js'; @@ -6,9 +7,12 @@ import './dialog.js'; import '../dialog-actions.js'; import '../dialog-content.js'; import '../dialog-title.js'; -import '../../link/block-link.js'; import '../../button/button.js'; import '../../button/secondary-button.js'; +import '../../form-field.js'; +import '../../link/block-link.js'; +import '../../option.js'; +import '../../select.js'; describe(`sbb-dialog`, () => { const negativeCases = [false, true]; @@ -112,5 +116,53 @@ describe(`sbb-dialog`, () => { setup.withPostSetupAction(() => dialog.open()); }), ); + + it( + `with scrolled select`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` + + ${dialogTitle()} + +

Content

+

Content

+

Content

+ + + Option 1 + Option 2 + + +

Content

+

Content

+

Content

+

Content

+

Content

+
+ ${dialogFooter()} +
+ `); + const dialog = setup.snapshotElement.querySelector('sbb-dialog')!; + const select = setup.snapshotElement.querySelector('sbb-select')!; + const scrollContext = setup.snapshotElement + .querySelector('sbb-dialog-content')! + .shadowRoot!.querySelector('.sbb-dialog-content')!; + + setup.withSnapshotElement(dialog); + + setup.withPostSetupAction(async () => { + dialog.open(); + + // We need to wait until the dialog is completely settled. + await aTimeout(40); + + select.click(); + scrollContext.scrollTo(0, 100); + + // We need to ensure content resizing kicked in + await aTimeout(0); + }); + }), + ); }); }); diff --git a/src/elements/menu/menu/menu.spec.ts b/src/elements/menu/menu/menu.spec.ts index bb40ed12a7..e314fefc99 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,14 +268,14 @@ 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'); }); diff --git a/src/elements/menu/menu/menu.ts b/src/elements/menu/menu/menu.ts index 724ac8e640..1dc49ec0b8 100644 --- a/src/elements/menu/menu/menu.ts +++ b/src/elements/menu/menu/menu.ts @@ -168,11 +168,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 +319,7 @@ class SbbMenuElement extends SbbNamedSlotListMixin< document.addEventListener('scroll', () => this._setMenuPosition(), { passive: true, signal: this._windowEventsController.signal, + capture: true, }); window.addEventListener('resize', () => this._setMenuPosition(), { passive: true, diff --git a/src/elements/menu/menu/menu.visual.spec.ts b/src/elements/menu/menu/menu.visual.spec.ts index f28f2b271b..fa9325ae53 100644 --- a/src/elements/menu/menu/menu.visual.spec.ts +++ b/src/elements/menu/menu/menu.visual.spec.ts @@ -1,3 +1,4 @@ +import { aTimeout } from '@open-wc/testing'; import { html } from 'lit'; import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; @@ -106,4 +107,43 @@ describe(`sbb-menu`, () => { }), ); }); + + describeViewports({ viewports: ['medium'], viewportHeight: 400 }, () => { + it( + `with scroll context`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` +
+

Content

+

Content

+

Content

+ + + Element 1 + Element 2 + Element 3 + +

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+
+ `); + const scrollContext = setup.snapshotElement.querySelector('div')!; + const menu = setup.snapshotElement.querySelector('sbb-menu')!; + + setup.withSnapshotElement(scrollContext); + + setup.withPostSetupAction(async () => { + menu.open(); + scrollContext.scrollTo(0, 100); + + // We need to ensure content resizing kicked in + await aTimeout(40); + }); + }), + ); + }); }); 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..252e9e39e0 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'); } @@ -61,8 +58,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 +75,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 +93,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 +109,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 +125,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 +172,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 +204,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 +218,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 +233,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'); diff --git a/src/elements/overlay/overlay.ts b/src/elements/overlay/overlay.ts index 5d729b79a8..307dac8f1f 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, forwardEventToHost } 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,10 @@ class SbbOverlayElement extends SbbOverlayBaseElement {
${this.backButton ? backButton : nothing} ${closeButton}
-
+
forwardEventToHost(e, document)} + > { const defaultArgs = { expanded: false, @@ -82,4 +87,52 @@ describe(`sbb-overlay`, () => { ); } }); + + describeViewports({ viewports: ['zero'], viewportHeight: 400 }, () => { + it( + `with scrolled autocomplete`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` + +

Content

+

Content

+

Content

+

Content

+ + + + Option 1 + Option 2 + + +

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+
+ `); + const overlay = setup.snapshotElement.querySelector('sbb-overlay')!; + const input = setup.snapshotElement.querySelector('input')!; + const scrollContext = + overlay.shadowRoot!.querySelector('.sbb-overlay__content')!; + + setup.withSnapshotElement(overlay); + + setup.withPostSetupAction(async () => { + overlay.open(); + + // We need to wait until the dialog is completely settled. + await aTimeout(40); + + input.focus(); + scrollContext.scrollTo(0, 100); + + // We need to ensure content resizing kicked in + await aTimeout(0); + }); + }), + ); + }); }); diff --git a/src/elements/popover/popover/popover.ts b/src/elements/popover/popover/popover.ts index b468b3c3e4..b07e9786a2 100644 --- a/src/elements/popover/popover/popover.ts +++ b/src/elements/popover/popover/popover.ts @@ -192,13 +192,13 @@ class SbbPopoverElement extends SbbHydrationMixin(SbbOpenCloseBaseElement) { private _handleOpening(): void { this.state = 'opened'; - this.didOpen.emit(); this.inert = false; this._attachWindowEvents(); this._setPopoverFocus(); this._focusHandler.trap(this, { postFilter: (el) => el !== this._overlay, }); + this.didOpen.emit(); } // Closes the popover on "Esc" key pressed and traps focus within the popover. @@ -317,6 +317,7 @@ class SbbPopoverElement extends SbbHydrationMixin(SbbOpenCloseBaseElement) { document.addEventListener('scroll', () => this._setPopoverPosition(), { passive: true, signal: this._openStateController.signal, + 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..fc620cbc5c 100644 --- a/src/elements/select/select.ts +++ b/src/elements/select/select.ts @@ -577,6 +577,7 @@ class SbbSelectElement extends SbbUpdateSchedulerMixin( document.addEventListener('scroll', () => this._setOverlayPosition(), { passive: true, signal: this._openPanelEventsController.signal, + capture: true, }); window.addEventListener('resize', () => this._setOverlayPosition(), { passive: true, diff --git a/src/elements/select/select.visual.spec.ts b/src/elements/select/select.visual.spec.ts index 14a16aa6e4..ee64df3f9d 100644 --- a/src/elements/select/select.visual.spec.ts +++ b/src/elements/select/select.visual.spec.ts @@ -1,3 +1,4 @@ +import { aTimeout } from '@open-wc/testing'; import { html, nothing, type TemplateResult } from 'lit'; import { describeViewports, visualDiffDefault, visualDiffFocus } from '../core/testing/private.js'; @@ -114,6 +115,44 @@ describe('sbb-select', () => { ); } } + + it( + `with scroll context`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` +
+

Content

+

Content

+

Content

+ + + Option 1 + Option 2 + Option 3 + + +

Content

+

Content

+

Content

+

Content

+

Content

+

Content

+
+ `); + const scrollContext = setup.snapshotElement.querySelector('div')!; + const select = setup.snapshotElement.querySelector('sbb-select')!; + + setup.withSnapshotElement(scrollContext); + + setup.withPostSetupAction(async () => { + select.open(); + scrollContext.scrollTo(0, 100); + + // We need to ensure content resizing kicked in + await aTimeout(40); + }); + }), + ); }); describeViewports({ viewports: ['zero', 'medium'] }, () => {