From 372ba1f66d08882540de100e876ff0c9af24719d Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Mon, 3 Feb 2025 20:26:54 +0100 Subject: [PATCH] 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 --- docs/pages/resources/changelog.md | 2 + src/components/alert/alert.component.ts | 25 ++++--- src/internal/scrollend-polyfill.ts | 89 +++++++++++++------------ 3 files changed, 67 insertions(+), 49 deletions(-) diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index abab066e..4436665c 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -16,6 +16,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti - Improved performance of `` when using a large number of options [#2318] - Updated the Japanese translation [#2329] +- Adjust `` 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 `` focus trapping. ## 2.19.1 diff --git a/src/components/alert/alert.component.ts b/src/components/alert/alert.component.ts index f25a5fda..2f2ffbd9 100644 --- a/src/components/alert/alert.component.ts +++ b/src/components/alert/alert.component.ts @@ -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(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 } diff --git a/src/internal/scrollend-polyfill.ts b/src/internal/scrollend-polyfill.ts index b58636c3..2eaaed05 100644 --- a/src/internal/scrollend-polyfill.ts +++ b/src/internal/scrollend-polyfill.ts @@ -26,54 +26,61 @@ const decorate = ( } as MethodOf; }; -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(); + 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(); - 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 {};