diff --git a/blocks/canvas/ew-panel-extensions/helpers.js b/blocks/canvas/ew-panel-extensions/helpers.js index 057d8d976..b013af80f 100644 --- a/blocks/canvas/ew-panel-extensions/helpers.js +++ b/blocks/canvas/ew-panel-extensions/helpers.js @@ -470,7 +470,29 @@ function createVersioningView() { }; } -function extensionToPanelView(ext, section) { +export function extensionToPanelView(ext, section) { + // Block library opens its own dedicated modal (used by the slash menu and + // outline "+" button) rather than the generic inline panel or iframe dialog. + if (ext.name === 'blocks') { + return { + id: ext.name, + label: ext.title, + section, + firstParty: ext.ootb, + experience: 'modal', + icon: ext.icon, + openModal: async () => { + const { openBlockLibraryModal } = await import('../ew-block-library-modal/ew-block-library-modal.js'); + openBlockLibraryModal({ + onInsert: (dom) => { + const { view } = getExtensionsBridge(); + if (view) insertBlock(view, dom); + }, + }); + }, + }; + } + const view = { id: ext.name, label: ext.title, @@ -530,8 +552,7 @@ function extensionToPanelView(ext, section) { */ export async function getCanvasToolPanelViews({ org, site }) { const extensions = await fetchExtensions(org, site); - const library = sortLibraryExtensions(extensions.filter(isLibraryExtension)) - .filter((ext) => ext.name !== 'blocks'); + const library = sortLibraryExtensions(extensions.filter(isLibraryExtension)); const thirdParty = extensions.filter((ext) => !isLibraryExtension(ext)); return [ diff --git a/blocks/canvas/ew-tool-panel/tool-panel.js b/blocks/canvas/ew-tool-panel/tool-panel.js index 242f888cc..3a5f8cb23 100644 --- a/blocks/canvas/ew-tool-panel/tool-panel.js +++ b/blocks/canvas/ew-tool-panel/tool-panel.js @@ -52,7 +52,8 @@ class EwToolPanel extends LitElement { items.push({ section: v.section }); lastSection = v.section; } - const opensExternally = v.experience === 'window' || v.experience === 'fullsize-dialog'; + const opensExternally = v.experience === 'window' || v.experience === 'fullsize-dialog' + || v.experience === 'modal'; items.push({ value: v.id, label: v.label, @@ -153,6 +154,10 @@ class EwToolPanel extends LitElement { this._fullsizeDialogViewId = name; return; } + if (consumer.experience === 'modal') { + await consumer.openModal?.(); + return; + } if (!this._loaded[name]) { this._loaded[name] = await consumer.load(); } diff --git a/test/unit/blocks/canvas/ew-panel-extensions/helpers.test.js b/test/unit/blocks/canvas/ew-panel-extensions/helpers.test.js index fcf2d0ef8..355971db9 100644 --- a/test/unit/blocks/canvas/ew-panel-extensions/helpers.test.js +++ b/test/unit/blocks/canvas/ew-panel-extensions/helpers.test.js @@ -4,10 +4,12 @@ import { setNx } from '../../../../../scripts/utils.js'; setNx('/test/fixtures/nx', { hostname: 'example.com' }); let getBlockVariants; +let extensionToPanelView; before(async () => { const mod = await import('../../../../../blocks/canvas/ew-panel-extensions/helpers.js'); getBlockVariants = mod.getBlockVariants; + extensionToPanelView = mod.extensionToPanelView; }); describe('EW panel helpers transformBlock', () => { @@ -179,3 +181,30 @@ describe('EW panel helpers transformBlock', () => { expect(dom.querySelector('p')).to.not.be.null; }); }); + +describe('extensionToPanelView', () => { + it('gives the "blocks" extension a dedicated modal experience', () => { + const ext = { name: 'blocks', title: 'Blocks', ootb: true, icon: '#icon-blocks' }; + const view = extensionToPanelView(ext, 'Library'); + expect(view.id).to.equal('blocks'); + expect(view.label).to.equal('Blocks'); + expect(view.section).to.equal('Library'); + expect(view.firstParty).to.be.true; + expect(view.experience).to.equal('modal'); + expect(view.icon).to.equal('#icon-blocks'); + expect(view.openModal).to.be.a('function'); + // The modal view opts out of the generic inline-panel loader. + expect(view.load).to.be.undefined; + }); + + it('leaves non-blocks extensions on the standard inline/load experience', () => { + const ext = { + name: 'templates', title: 'Templates', ootb: true, experience: 'inline', sources: ['/tpl'], icon: '', + }; + const view = extensionToPanelView(ext, 'Library'); + expect(view.id).to.equal('templates'); + expect(view.experience).to.equal('inline'); + expect(view.load).to.be.a('function'); + expect(view.openModal).to.be.undefined; + }); +}); diff --git a/test/unit/blocks/canvas/ew-tool-panel/tool-panel.test.js b/test/unit/blocks/canvas/ew-tool-panel/tool-panel.test.js new file mode 100644 index 000000000..205576cda --- /dev/null +++ b/test/unit/blocks/canvas/ew-tool-panel/tool-panel.test.js @@ -0,0 +1,64 @@ +/* eslint-disable no-underscore-dangle */ +import { expect } from '@esm-bundle/chai'; +import { setNx } from '../../../../../scripts/utils.js'; + +setNx('/test/fixtures/nx', { hostname: 'example.com' }); + +before(async () => { + await import('../../../../../blocks/canvas/ew-tool-panel/tool-panel.js'); +}); + +// The element is used detached (never appended), so Lit never runs an update +// cycle — we call the changed methods directly. The 'modal' branch of showPanel +// returns before touching the shadow root, so this stays safe without rendering. +function createPanel(views) { + const el = document.createElement('ew-tool-panel'); + el.views = views; + return el; +} + +describe('EwToolPanel — modal experience', () => { + describe('_pickerItemsFromViews', () => { + it('marks a modal view as an external-opening action item', () => { + const el = createPanel([ + { id: 'blocks', label: 'Blocks', section: 'Library', experience: 'modal' }, + ]); + const item = el._pickerItemsFromViews().find((i) => i.value === 'blocks'); + expect(item.action).to.be.true; + expect(item.trailingIcon).to.exist; + expect(item.ariaLabel).to.equal('Blocks (opens in dialog)'); + }); + + it('leaves a plain inline view as a non-action item', () => { + const el = createPanel([ + { id: 'files', label: 'Files', section: 'Editor', experience: 'inline' }, + ]); + const item = el._pickerItemsFromViews().find((i) => i.value === 'files'); + expect(item.action).to.be.undefined; + expect(item.trailingIcon).to.be.undefined; + }); + }); + + describe('showPanel', () => { + it('invokes openModal and does not activate the view for a modal experience', async () => { + let opened = 0; + const openModal = async () => { opened += 1; }; + const el = createPanel([{ id: 'blocks', label: 'Blocks', experience: 'modal', openModal }]); + await el.showPanel('blocks'); + expect(opened).to.equal(1); + expect(el.activeId).to.be.undefined; + }); + + it('does not throw when a modal view has no openModal handler', async () => { + const el = createPanel([{ id: 'blocks', label: 'Blocks', experience: 'modal' }]); + let threw = false; + try { + await el.showPanel('blocks'); + } catch { + threw = true; + } + expect(threw).to.be.false; + expect(el.activeId).to.be.undefined; + }); + }); +});