fix ssr for sl-alert and scrollend-polyfill (#2359)

* 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 <cory@abeautifulsite.net>
This commit is contained in:
Christian Schilling
2025-02-03 20:26:54 +01:00
committed by GitHub
parent b0399ca74e
commit 372ba1f66d
3 changed files with 67 additions and 49 deletions

View File

@@ -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

View File

@@ -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
@@ -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;
@@ -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(() => {
@@ -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 }

View File

@@ -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);
}
};
if (!isSupported) {
const pointers = new Set();
const scrollHandlers = new WeakMap<EventTarget, EventListenerOrEventListenerObject>();
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();
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);
});
const handlePointerUp = (event: TouchEvent) => {
for (const touch of event.changedTouches) {
pointers.delete(touch.identifier);
}
};
decorate(EventTarget.prototype, 'removeEventListener', function (this: EventTarget, removeEventListener, type) {
if (type !== 'scrollend') return;
document.addEventListener('touchstart', handlePointerDown, true);
document.addEventListener('touchend', handlePointerUp, true);
document.addEventListener('touchcancel', handlePointerUp, true);
const scrollHandler = scrollHandlers.get(this);
if (scrollHandler) {
removeEventListener.call(this, 'scroll', scrollHandler, { passive: true } as unknown as EventListenerOptions);
}
});
}
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 {};