Skip to content

Commit

Permalink
fix ssr for sl-alert and scrollend-polyfill (#2359)
Browse files Browse the repository at this point in the history
* Fix: Make Alert SSR compatible

* Fix: Make sure polyfill is only called on the frontend

* Add changelog entries

* Removed debug statement

* Changelog adjustments

* Another console.log statement :(

---------

Co-authored-by: Cory LaViska <[email protected]>
  • Loading branch information
schilchSICKAG and claviska authored Feb 3, 2025
1 parent b0399ca commit 372ba1f
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 51 deletions.
2 changes: 2 additions & 0 deletions docs/pages/resources/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti

- Improved performance of `<sl-select>` when using a large number of options [#2318]
- Updated the Japanese translation [#2329]
- Adjust `<sl-alert>` to create the toast stack when used only, making it usable in SSR environments. [#2359]
- Adjust `scrollend-polyfill` so it only runs on the client to make it usable in SSR environments. [#2359]
- Fixed a bug with radios in `<sl-dialog>` focus trapping.

## 2.19.1
Expand Down
25 changes: 17 additions & 8 deletions src/components/alert/alert.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import SlIconButton from '../icon-button/icon-button.component.js';
import styles from './alert.styles.js';
import type { CSSResultGroup } from 'lit';

const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });

/**
* @summary Alerts are used to display important messages inline or as toast notifications.
* @documentation https://shoelace.style/components/alert
Expand Down Expand Up @@ -50,6 +48,17 @@ export default class SlAlert extends ShoelaceElement {
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
private readonly localize = new LocalizeController(this);

private static currentToastStack: HTMLDivElement;

private static get toastStack() {
if (!this.currentToastStack) {
this.currentToastStack = Object.assign(document.createElement('div'), {
className: 'sl-toast-stack'
});
}
return this.currentToastStack;
}

@query('[part~="base"]') base: HTMLElement;

@query('.alert__countdown-elapsed') countdownElement: HTMLElement;
Expand Down Expand Up @@ -195,11 +204,11 @@ export default class SlAlert extends ShoelaceElement {
async toast() {
return new Promise<void>(resolve => {
this.handleCountdownChange();
if (toastStack.parentElement === null) {
document.body.append(toastStack);
if (SlAlert.toastStack.parentElement === null) {
document.body.append(SlAlert.toastStack);
}

toastStack.appendChild(this);
SlAlert.toastStack.appendChild(this);

// Wait for the toast stack to render
requestAnimationFrame(() => {
Expand All @@ -211,12 +220,12 @@ export default class SlAlert extends ShoelaceElement {
this.addEventListener(
'sl-after-hide',
() => {
toastStack.removeChild(this);
SlAlert.toastStack.removeChild(this);
resolve();

// Remove the toast stack from the DOM when there are no more alerts
if (toastStack.querySelector('sl-alert') === null) {
toastStack.remove();
if (SlAlert.toastStack.querySelector('sl-alert') === null) {
SlAlert.toastStack.remove();
}
},
{ once: true }
Expand Down
93 changes: 50 additions & 43 deletions src/internal/scrollend-polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,54 +26,61 @@ const decorate = <T, M extends keyof T>(
} as MethodOf<T, M>;
};

const isSupported = 'onscrollend' in window;
(() => {
// SSR environments should not apply the polyfill
if (typeof window === 'undefined') {
return;
}

if (!isSupported) {
const pointers = new Set();
const scrollHandlers = new WeakMap<EventTarget, EventListenerOrEventListenerObject>();
const isSupported = 'onscrollend' in window;

const handlePointerDown = (event: TouchEvent) => {
for (const touch of event.changedTouches) {
pointers.add(touch.identifier);
}
};

const handlePointerUp = (event: TouchEvent) => {
for (const touch of event.changedTouches) {
pointers.delete(touch.identifier);
}
};

document.addEventListener('touchstart', handlePointerDown, true);
document.addEventListener('touchend', handlePointerUp, true);
document.addEventListener('touchcancel', handlePointerUp, true);

decorate(EventTarget.prototype, 'addEventListener', function (this: EventTarget, addEventListener, type) {
if (type !== 'scrollend') return;
if (!isSupported) {
const pointers = new Set();
const scrollHandlers = new WeakMap<EventTarget, EventListenerOrEventListenerObject>();

const handleScrollEnd = debounce(() => {
if (!pointers.size) {
// If no pointer is active in the scroll area then the scroll has ended
this.dispatchEvent(new Event('scrollend'));
} else {
// otherwise let's wait a bit more
handleScrollEnd();
const handlePointerDown = (event: TouchEvent) => {
for (const touch of event.changedTouches) {
pointers.add(touch.identifier);
}
}, 100);
};

addEventListener.call(this, 'scroll', handleScrollEnd, { passive: true });
scrollHandlers.set(this, handleScrollEnd);
});

decorate(EventTarget.prototype, 'removeEventListener', function (this: EventTarget, removeEventListener, type) {
if (type !== 'scrollend') return;

const scrollHandler = scrollHandlers.get(this);
if (scrollHandler) {
removeEventListener.call(this, 'scroll', scrollHandler, { passive: true } as unknown as EventListenerOptions);
}
});
}
const handlePointerUp = (event: TouchEvent) => {
for (const touch of event.changedTouches) {
pointers.delete(touch.identifier);
}
};

document.addEventListener('touchstart', handlePointerDown, true);
document.addEventListener('touchend', handlePointerUp, true);
document.addEventListener('touchcancel', handlePointerUp, true);

decorate(EventTarget.prototype, 'addEventListener', function (this: EventTarget, addEventListener, type) {
if (type !== 'scrollend') return;

const handleScrollEnd = debounce(() => {
if (!pointers.size) {
// If no pointer is active in the scroll area then the scroll has ended
this.dispatchEvent(new Event('scrollend'));
} else {
// otherwise let's wait a bit more
handleScrollEnd();
}
}, 100);

addEventListener.call(this, 'scroll', handleScrollEnd, { passive: true });
scrollHandlers.set(this, handleScrollEnd);
});

decorate(EventTarget.prototype, 'removeEventListener', function (this: EventTarget, removeEventListener, type) {
if (type !== 'scrollend') return;

const scrollHandler = scrollHandlers.get(this);
if (scrollHandler) {
removeEventListener.call(this, 'scroll', scrollHandler, { passive: true } as unknown as EventListenerOptions);
}
});
}
})();

// Without an import or export, TypeScript sees vars in this file as global
export {};

0 comments on commit 372ba1f

Please sign in to comment.