From e3c849236a6e4a9c446c4347a8750558a228ed80 Mon Sep 17 00:00:00 2001 From: Mark Daugherty Date: Fri, 17 Jul 2026 13:57:43 -0500 Subject: [PATCH 1/2] feat(edit): show loading spinner while pages and sheets load Pages previously rendered an empty editor until content loaded. Add a centered spinner (reusing the shared .da-loading-spinner pattern) that shows in the page editor until the document has synced and rendered, and in the sheet view until data is fetched and the grid is rendered. Co-Authored-By: Claude Opus 4.8 (1M context) --- blocks/edit/da-content/da-content.css | 25 +++++++++++++++++++++++++ blocks/edit/da-content/da-content.js | 5 +++++ blocks/edit/prose/index.js | 6 ++++-- blocks/sheet/sheet.css | 25 +++++++++++++++++++++++++ blocks/sheet/sheet.js | 10 +++++++++- 5 files changed, 68 insertions(+), 3 deletions(-) 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; }); } @@ -526,7 +528,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 7ca441de1..4a04e7254 100644 --- a/blocks/sheet/sheet.js +++ b/blocks/sheet/sheet.js @@ -118,7 +118,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; } From 759728ee3bc7dc150ba50934f94e20fa5d6aa926 Mon Sep 17 00:00:00 2001 From: Mark Daugherty Date: Fri, 17 Jul 2026 15:49:31 -0500 Subject: [PATCH 2/2] test(edit): cover loading spinner and contentReady behavior Add a da-content test that mounts the component and asserts the loading spinner shows until contentReady is set, and extend the prose initProse sync test to assert contentReady is set once the WebSocket syncs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../blocks/edit/da-content/da-content.test.js | 22 +++++++++++++++++++ test/unit/blocks/edit/prose/index.test.js | 2 ++ 2 files changed, 24 insertions(+) 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 a90500135..d888e17dd 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 () => {