diff --git a/packages/dashboard/src/vaadin-dashboard.d.ts b/packages/dashboard/src/vaadin-dashboard.d.ts index 72665eb9c60..e807870d961 100644 --- a/packages/dashboard/src/vaadin-dashboard.d.ts +++ b/packages/dashboard/src/vaadin-dashboard.d.ts @@ -85,6 +85,17 @@ export type DashboardItemResizedEvent = CustomEvent items: Array>; }>; +/** + * Fired before an item is removed. Calling preventDefault() on the event will cancel the removal. + */ +export type DashboardItemBeforeRemoveEvent = CustomEvent<{ + item: TItem | DashboardSectionItem; + + items: Array>; + + section: DashboardSectionItem | undefined; +}>; + /** * Fired when an item was removed */ @@ -92,6 +103,8 @@ export type DashboardItemRemovedEvent = CustomEvent item: TItem | DashboardSectionItem; items: Array>; + + section: DashboardSectionItem | undefined; }>; /** @@ -123,6 +136,8 @@ export interface DashboardCustomEventMap { 'dashboard-item-resized': DashboardItemResizedEvent; + 'dashboard-item-before-remove': DashboardItemBeforeRemoveEvent; + 'dashboard-item-removed': DashboardItemRemovedEvent; 'dashboard-item-selected-changed': DashboardItemSelectedChangedEvent; diff --git a/packages/dashboard/src/vaadin-dashboard.js b/packages/dashboard/src/vaadin-dashboard.js index a1bff9a5888..b98b1dcf8c7 100644 --- a/packages/dashboard/src/vaadin-dashboard.js +++ b/packages/dashboard/src/vaadin-dashboard.js @@ -93,6 +93,7 @@ import { WidgetResizeController } from './widget-resize-controller.js'; * * @fires {CustomEvent} dashboard-item-moved - Fired when an item was moved * @fires {CustomEvent} dashboard-item-resized - Fired when an item was resized + * @fires {CustomEvent} dashboard-item-before-remove - Fired before an item is removed. Calling preventDefault() on the event will cancel the removal. * @fires {CustomEvent} dashboard-item-removed - Fired when an item was removed * @fires {CustomEvent} dashboard-item-selected-changed - Fired when an item selected state changed * @fires {CustomEvent} dashboard-item-move-mode-changed - Fired when an item move mode changed @@ -427,12 +428,25 @@ class Dashboard extends DashboardLayoutMixin( e.stopImmediatePropagation(); const item = getElementItem(e.target); const items = getItemsArrayOfItem(item, this.items); + const section = this.items.find((i) => i.items && i.items.includes(item)); + + // Fire before-remove event + const beforeRemoveEvent = new CustomEvent('dashboard-item-before-remove', { + cancelable: true, + detail: { item, items: this.items, section }, + }); + this.dispatchEvent(beforeRemoveEvent); + + // Check if removal was prevented + if (beforeRemoveEvent.defaultPrevented) { + return; + } + + // Proceed with removal items.splice(items.indexOf(item), 1); this.items = [...this.items]; this.toggleAttribute('item-selected', false); - this.dispatchEvent( - new CustomEvent('dashboard-item-removed', { cancelable: true, detail: { item, items: this.items } }), - ); + this.dispatchEvent(new CustomEvent('dashboard-item-removed', { detail: { item, items: this.items, section } })); } /** @private */ @@ -516,6 +530,12 @@ class Dashboard extends DashboardLayoutMixin( * @event dashboard-item-resized */ + /** + * Fired before an item is removed + * + * @event dashboard-item-before-remove + */ + /** * Fired when an item was removed * diff --git a/packages/dashboard/test/dashboard.test.ts b/packages/dashboard/test/dashboard.test.ts index f449ab9ce12..fd4d26aa9af 100644 --- a/packages/dashboard/test/dashboard.test.ts +++ b/packages/dashboard/test/dashboard.test.ts @@ -107,6 +107,74 @@ describe('dashboard', () => { expect(dashboard.items).to.eql([{ id: '0' }]); }); + it('should dispatch a dashboard-item-before-remove event before removal', () => { + const spy = sinon.spy(); + dashboard.addEventListener('dashboard-item-before-remove', spy); + const widget = getElementFromCell(dashboard, 0, 1); + getRemoveButton(widget as DashboardWidget).click(); + expect(spy).to.be.calledOnce; + expect(spy.firstCall.args[0].detail.item).to.eql({ id: '1' }); + expect(spy.firstCall.args[0].detail.items).to.eql([{ id: '0' }]); // contains the state after removal + expect(spy.firstCall.args[0].detail.section).to.be.undefined; + }); + + it('should include section in dashboard-item-before-remove event for nested items', async () => { + const sectionItem: DashboardSectionItem = { + title: 'Section', + items: [{ id: '2' }, { id: '3' }], + }; + dashboard.items = [{ id: '0' }, { id: '1' }, sectionItem]; + await updateComplete(dashboard); + + const spy = sinon.spy(); + dashboard.addEventListener('dashboard-item-before-remove', spy); + const widget = getElementFromCell(dashboard, 1, 0); + const section = widget?.closest('vaadin-dashboard-section'); + expect(section).to.be.ok; + getRemoveButton(widget as DashboardWidget).click(); + + expect(spy).to.be.calledOnce; + expect(spy.firstCall.args[0].detail.item).to.eql({ id: '2' }); + expect(spy.firstCall.args[0].detail.section).to.equal(sectionItem); + }); + + it('should cancel removal when preventDefault is called on dashboard-item-before-remove', () => { + const originalItems = dashboard.items; + dashboard.addEventListener('dashboard-item-before-remove', (e) => { + e.preventDefault(); + }); + const widget = getElementFromCell(dashboard, 0, 1); + getRemoveButton(widget as DashboardWidget).click(); + expect(dashboard.items).to.equal(originalItems); + expect(dashboard.items).to.eql([{ id: '0' }, { id: '1' }]); + }); + + it('should not fire dashboard-item-removed when dashboard-item-before-remove is prevented', () => { + const beforeRemoveSpy = sinon.spy(); + const removedSpy = sinon.spy(); + dashboard.addEventListener('dashboard-item-before-remove', (e) => { + beforeRemoveSpy(); + e.preventDefault(); + }); + dashboard.addEventListener('dashboard-item-removed', removedSpy); + const widget = getElementFromCell(dashboard, 0, 1); + getRemoveButton(widget as DashboardWidget).click(); + expect(beforeRemoveSpy).to.be.calledOnce; + expect(removedSpy).to.not.be.called; + }); + + it('should fire both events in correct order when not prevented', () => { + const beforeRemoveSpy = sinon.spy(); + const removedSpy = sinon.spy(); + dashboard.addEventListener('dashboard-item-before-remove', beforeRemoveSpy); + dashboard.addEventListener('dashboard-item-removed', removedSpy); + const widget = getElementFromCell(dashboard, 0, 1); + getRemoveButton(widget as DashboardWidget).click(); + expect(beforeRemoveSpy).to.be.calledOnce; + expect(removedSpy).to.be.calledOnce; + expect(beforeRemoveSpy).to.have.been.calledBefore(removedSpy); + }); + it('should dispatch an dashboard-item-removed event', () => { const spy = sinon.spy(); dashboard.addEventListener('dashboard-item-removed', spy); @@ -115,6 +183,27 @@ describe('dashboard', () => { expect(spy).to.be.calledOnce; expect(spy.firstCall.args[0].detail.item).to.eql({ id: '1' }); expect(spy.firstCall.args[0].detail.items).to.eql([{ id: '0' }]); + expect(spy.firstCall.args[0].detail.section).to.be.undefined; + }); + + it('should include section in dashboard-item-removed event for nested items', async () => { + const sectionItem: DashboardSectionItem = { + title: 'Section', + items: [{ id: '2' }, { id: '3' }], + }; + dashboard.items = [{ id: '0' }, { id: '1' }, sectionItem]; + await updateComplete(dashboard); + + const spy = sinon.spy(); + dashboard.addEventListener('dashboard-item-removed', spy); + const widget = getElementFromCell(dashboard, 1, 0); + const section = widget?.closest('vaadin-dashboard-section'); + expect(section).to.be.ok; + getRemoveButton(widget as DashboardWidget).click(); + + expect(spy).to.be.calledOnce; + expect(spy.firstCall.args[0].detail.item).to.eql({ id: '2' }); + expect(spy.firstCall.args[0].detail.section).to.equal(sectionItem); }); it('should not dispatch an item-remove event', async () => { diff --git a/packages/dashboard/test/typings/dashboard.types.ts b/packages/dashboard/test/typings/dashboard.types.ts index c1ef28f99c3..e300eeb26d8 100644 --- a/packages/dashboard/test/typings/dashboard.types.ts +++ b/packages/dashboard/test/typings/dashboard.types.ts @@ -5,6 +5,7 @@ import type { Dashboard, DashboardI18n, DashboardItem, + DashboardItemBeforeRemoveEvent, DashboardItemMovedEvent, DashboardItemMoveModeChangedEvent, DashboardItemRemovedEvent, @@ -90,11 +91,20 @@ narrowedDashboard.addEventListener('dashboard-item-resized', (event) => { assertType>>(event.detail.items); }); +narrowedDashboard.addEventListener('dashboard-item-before-remove', (event) => { + assertType>(event); + assertType(event.detail.item as TestDashboardItem); + assertType>(event.detail.item as DashboardSectionItem); + assertType>>(event.detail.items); + assertType | undefined>(event.detail.section); +}); + narrowedDashboard.addEventListener('dashboard-item-removed', (event) => { assertType>(event); assertType(event.detail.item as TestDashboardItem); assertType>(event.detail.item as DashboardSectionItem); assertType>>(event.detail.items); + assertType | undefined>(event.detail.section); }); narrowedDashboard.addEventListener('dashboard-item-selected-changed', (event) => {