diff --git a/blocks/edit/da-content/da-content.css b/blocks/edit/da-content/da-content.css
index 2572099c3..8509dd7cd 100644
--- a/blocks/edit/da-content/da-content.css
+++ b/blocks/edit/da-content/da-content.css
@@ -15,6 +15,31 @@
width: var(--se-grid-container-width, var(--grid-container-width));
}
+/* Loading spinner shown until the document content has synced and rendered */
+@keyframes da-spin {
+ to { transform: rotate(360deg); }
+}
+
+.da-editor-loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 600px;
+ z-index: 2;
+}
+
+.da-loading-spinner {
+ display: block;
+ width: 32px;
+ height: 32px;
+ border: 3px solid var(--s2-gray-300);
+ border-top-color: var(--s2-gray-700);
+ border-radius: 50%;
+ animation: da-spin 0.8s linear infinite;
+}
+
da-editor {
position: relative;
z-index: 1;
diff --git a/blocks/edit/da-content/da-content.js b/blocks/edit/da-content/da-content.js
index c9fbc7130..f8bbc48c9 100644
--- a/blocks/edit/da-content/da-content.js
+++ b/blocks/edit/da-content/da-content.js
@@ -11,6 +11,7 @@ export default class DaContent extends LitElement {
details: { attribute: false },
permissions: { attribute: false },
proseEl: { attribute: false },
+ contentReady: { attribute: false },
wsProvider: { attribute: false },
_editorLoaded: { state: true },
_showPane: { state: true },
@@ -79,6 +80,10 @@ export default class DaContent extends LitElement {
return html`
+ ${this.contentReady ? nothing : html`
+
+
+
`}
{
if (canWrite) {
const pm = document.querySelector('da-content')?.shadowRoot
.querySelector('da-editor')?.shadowRoot.querySelector('.ProseMirror');
if (pm) pm.contentEditable = 'true';
}
+ // Content has synced and rendered — clear the loading spinner.
+ if (daContent) daContent.contentReady = true;
});
}
@@ -533,7 +535,7 @@ export default async function initProse({ path, permissions, doc, daContent, wsP
const { wsProvider, ydoc } = await connectionPromise;
- addSyncedListener(wsProvider, canWrite);
+ addSyncedListener(wsProvider, canWrite, daContent);
createAwarenessStatusWidget(wsProvider, window, path);
registerErrorHandler(ydoc);
diff --git a/blocks/sheet/sheet.css b/blocks/sheet/sheet.css
index b8ad166be..a436557be 100644
--- a/blocks/sheet/sheet.css
+++ b/blocks/sheet/sheet.css
@@ -16,6 +16,31 @@ body {
width: 200px;
}
+/* Loading spinner shown until the sheet has fetched and rendered */
+@keyframes da-spin {
+ to { transform: rotate(360deg); }
+}
+
+.da-sheet-loading {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 300px;
+ z-index: 20;
+}
+
+.da-loading-spinner {
+ display: block;
+ width: 32px;
+ height: 32px;
+ border: 3px solid var(--s2-gray-300);
+ border-top-color: var(--s2-gray-700);
+ border-radius: 50%;
+ animation: da-spin 0.8s linear infinite;
+}
+
.da-version-wrapper {
display: flex;
justify-content: center;
diff --git a/blocks/sheet/sheet.js b/blocks/sheet/sheet.js
index eaca19b3c..4280b8ec6 100644
--- a/blocks/sheet/sheet.js
+++ b/blocks/sheet/sheet.js
@@ -117,7 +117,15 @@ let initSheet;
async function reloadSheet(daTitle, daSheet) {
if (!initSheet) initSheet = (await import('./utils/index.js')).default;
- daTitle.sheet = await initSheet(daSheet);
+ const loader = document.createElement('div');
+ loader.className = 'da-sheet-loading';
+ loader.innerHTML = '';
+ daSheet.closest('.da-sheet-wrapper')?.append(loader);
+ try {
+ daTitle.sheet = await initSheet(daSheet);
+ } finally {
+ loader.remove();
+ }
daTitle.disabledText = undefined;
}
diff --git a/test/unit/blocks/edit/da-content/da-content.test.js b/test/unit/blocks/edit/da-content/da-content.test.js
index 12c9097be..d5c8c222c 100644
--- a/test/unit/blocks/edit/da-content/da-content.test.js
+++ b/test/unit/blocks/edit/da-content/da-content.test.js
@@ -64,4 +64,26 @@ describe('da-content', () => {
expect(viewsCalled).to.be.true;
expect(ueCalled).to.be.true;
});
+
+ it('shows a loading spinner until content is ready', async () => {
+ const ed = document.createElement('da-content');
+ ed.details = {
+ owner: 'o',
+ repo: 'r',
+ previewUrl: 'https://main--r--o.aem.live/p',
+ };
+ document.body.appendChild(ed);
+ await ed.updateComplete;
+
+ // Spinner is shown while the document has not synced/rendered.
+ expect(ed.shadowRoot.querySelector('.da-editor-loading')).to.exist;
+ expect(ed.shadowRoot.querySelector('.da-loading-spinner')).to.exist;
+
+ // It clears once content is ready.
+ ed.contentReady = true;
+ await ed.updateComplete;
+ expect(ed.shadowRoot.querySelector('.da-editor-loading')).to.be.null;
+
+ ed.remove();
+ });
});
diff --git a/test/unit/blocks/edit/prose/index.test.js b/test/unit/blocks/edit/prose/index.test.js
index ab31501e3..337d1a21e 100644
--- a/test/unit/blocks/edit/prose/index.test.js
+++ b/test/unit/blocks/edit/prose/index.test.js
@@ -801,6 +801,8 @@ describe('prose/index initProse default export', () => {
expect(fakeContent.wsProvider).to.equal(provider);
// Wait for handleProseLoaded's setTimeout
await wait(20);
+ // Once synced, the loading spinner is cleared via daContent.contentReady.
+ expect(fakeContent.contentReady).to.be.true;
});
it('Reads-only when permissions has no write', async () => {