Skip to content

Commit

Permalink
fix: handle scroll events in custom scroll contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
jeripeierSBB committed Dec 19, 2024
1 parent fd55843 commit 0a8415a
Show file tree
Hide file tree
Showing 20 changed files with 129 additions and 119 deletions.
3 changes: 3 additions & 0 deletions src/elements/autocomplete/autocomplete-base-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 11 additions & 4 deletions src/elements/core/eventing/forward-event.ts
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 2 additions & 2 deletions src/elements/datepicker/datepicker/datepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -243,7 +243,7 @@ class SbbDatepickerElement<T = Date> extends LitElement {
input.addEventListener(
'input',
(e) => {
forwardEventToHost(e, this);
forwardEvent(e, this);
this._parseInput();
},
options,
Expand Down
4 changes: 3 additions & 1 deletion src/elements/dialog/dialog-content/dialog-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -16,7 +18,7 @@ class SbbDialogContentElement extends LitElement {

protected override render(): TemplateResult {
return html`
<div class="sbb-dialog-content">
<div class="sbb-dialog-content" @scroll=${(e: Event) => forwardEvent(e, document)}>
<slot></slot>
</div>
`;
Expand Down
3 changes: 3 additions & 0 deletions src/elements/dialog/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
2 changes: 1 addition & 1 deletion src/elements/dialog/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ class SbbDialogElement extends SbbOverlayBaseElement {

private _handleOpening(): void {
this.state = 'opened';
this.didOpen.emit();
this.inertController.activate();
this.attachOpenOverlayEvents();
this.setOverlayFocus();
Expand All @@ -131,6 +130,7 @@ class SbbDialogElement extends SbbOverlayBaseElement {
),
);
this.focusHandler.trap(this);
this.didOpen.emit();
}

public override connectedCallback(): void {
Expand Down
4 changes: 2 additions & 2 deletions src/elements/file-selector/common/file-selector-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -183,7 +183,7 @@ export const SbbFileSelectorCommonElementMixin = <T extends Constructor<LitEleme
if (fileInput.files) {
this.createFileList(fileInput.files);
}
forwardEventToHost(event, this);
forwardEvent(event, this);
}

protected createFileList(files: FileList): void {
Expand Down
40 changes: 36 additions & 4 deletions src/elements/map-container/map-container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { html } from 'lit/static-html.js';

import type { SbbAccentButtonElement } from '../button.js';
import { fixture } from '../core/testing/private.js';
import { waitForCondition } from '../core/testing.js';
import { EventSpy, waitForCondition } from '../core/testing.js';

import { SbbMapContainerElement } from './map-container.js';

Expand All @@ -14,10 +14,8 @@ describe(`sbb-map-container`, () => {
let element: SbbMapContainerElement;

it('should react to scrolling', async () => {
await setViewport({ width: 320, height: 600 });

element = await fixture(
html` <sbb-map-container>
html`<sbb-map-container>
<div>
<sbb-title level="4">Operations & Disruptions</sbb-title>
<div><p>Situation 1</p></div>
Expand All @@ -34,6 +32,9 @@ describe(`sbb-map-container`, () => {
</div>
</sbb-map-container>`,
);

await setViewport({ width: 320, height: 600 });

assert.instanceOf(element, SbbMapContainerElement);

function getInert(): boolean {
Expand All @@ -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`<sbb-map-container>
<div>
<sbb-title level="4">Operations & Disruptions</sbb-title>
<div><p>Situation 1</p></div>
<div><p>Situation 2</p></div>
<div><p>Situation 3</p></div>
<div><p>Situation 4</p></div>
<div><p>Situation 5</p></div>
<div><p>Situation 6</p></div>
<div><p>Situation 7</p></div>
<div><p>Situation 8</p></div>
</div>
<div slot="map">
<div style="height: 300px">map</div>
</div>
</sbb-map-container>`,
);

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);
});
});
3 changes: 2 additions & 1 deletion src/elements/map-container/map-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -96,7 +97,7 @@ class SbbMapContainerElement extends LitElement {
<div class="sbb-map-container__map">
<slot name="map"></slot>
</div>
<div class="sbb-map-container__sidebar">
<div class="sbb-map-container__sidebar" @scroll=${(e: Event) => forwardEvent(e, document)}>
<span id="intersector"></span>
<slot></slot>
Expand Down
46 changes: 20 additions & 26 deletions src/elements/menu/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand All @@ -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 });
Expand All @@ -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');
});
Expand All @@ -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;
Expand All @@ -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');
});

Expand All @@ -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;
Expand All @@ -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');
});
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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');

Expand All @@ -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');
});

Expand All @@ -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);
});
});
Loading

0 comments on commit 0a8415a

Please sign in to comment.