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