From 366e2e5cff1c5bbeec5ab6d7dd1c0dea46b4ac68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Thu, 11 Dec 2025 12:40:39 +0100 Subject: [PATCH 1/3] implement before remove event --- packages/dashboard/src/vaadin-dashboard.d.ts | 11 +++++ packages/dashboard/src/vaadin-dashboard.js | 28 +++++++++-- packages/dashboard/test/dashboard.test.ts | 47 +++++++++++++++++++ .../dashboard/test/typings/dashboard.types.ts | 8 ++++ 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/packages/dashboard/src/vaadin-dashboard.d.ts b/packages/dashboard/src/vaadin-dashboard.d.ts index 72665eb9c60..5058f4048b4 100644 --- a/packages/dashboard/src/vaadin-dashboard.d.ts +++ b/packages/dashboard/src/vaadin-dashboard.d.ts @@ -85,6 +85,15 @@ 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>; +}>; + /** * Fired when an item was removed */ @@ -123,6 +132,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..348a24a8ae5 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,24 @@ class Dashboard extends DashboardLayoutMixin( e.stopImmediatePropagation(); const item = getElementItem(e.target); const items = getItemsArrayOfItem(item, this.items); + + // Fire before-remove event + const beforeRemoveEvent = new CustomEvent('dashboard-item-before-remove', { + cancelable: true, + detail: { item, items: [...this.items] }, + }); + 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 } })); } /** @private */ @@ -516,6 +529,15 @@ class Dashboard extends DashboardLayoutMixin( * @event dashboard-item-resized */ + /** + * Fired before an item is removed + * + * @event dashboard-item-before-remove + * @param {Object} detail + * @param {Object} detail.item the item to be removed + * @param {Array} detail.items the current items array + */ + /** * Fired when an item was removed * diff --git a/packages/dashboard/test/dashboard.test.ts b/packages/dashboard/test/dashboard.test.ts index f449ab9ce12..3b23987fb06 100644 --- a/packages/dashboard/test/dashboard.test.ts +++ b/packages/dashboard/test/dashboard.test.ts @@ -107,6 +107,53 @@ 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' }, { id: '1' }]); + }); + + 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); diff --git a/packages/dashboard/test/typings/dashboard.types.ts b/packages/dashboard/test/typings/dashboard.types.ts index c1ef28f99c3..fbf35e49caa 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,6 +91,13 @@ 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); +}); + narrowedDashboard.addEventListener('dashboard-item-removed', (event) => { assertType>(event); assertType(event.detail.item as TestDashboardItem); From 12a262fce06db2e40481c5ac54454f948982fad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Thu, 11 Dec 2025 13:21:37 +0100 Subject: [PATCH 2/3] add section to event --- packages/dashboard/src/vaadin-dashboard.d.ts | 4 ++ packages/dashboard/src/vaadin-dashboard.js | 5 ++- packages/dashboard/test/dashboard.test.ts | 42 +++++++++++++++++++ .../dashboard/test/typings/dashboard.types.ts | 2 + 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/dashboard/src/vaadin-dashboard.d.ts b/packages/dashboard/src/vaadin-dashboard.d.ts index 5058f4048b4..e807870d961 100644 --- a/packages/dashboard/src/vaadin-dashboard.d.ts +++ b/packages/dashboard/src/vaadin-dashboard.d.ts @@ -92,6 +92,8 @@ export type DashboardItemBeforeRemoveEvent = Custom item: TItem | DashboardSectionItem; items: Array>; + + section: DashboardSectionItem | undefined; }>; /** @@ -101,6 +103,8 @@ export type DashboardItemRemovedEvent = CustomEvent item: TItem | DashboardSectionItem; items: Array>; + + section: DashboardSectionItem | undefined; }>; /** diff --git a/packages/dashboard/src/vaadin-dashboard.js b/packages/dashboard/src/vaadin-dashboard.js index 348a24a8ae5..40068548a21 100644 --- a/packages/dashboard/src/vaadin-dashboard.js +++ b/packages/dashboard/src/vaadin-dashboard.js @@ -428,11 +428,12 @@ 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] }, + detail: { item, items: [...this.items], section }, }); this.dispatchEvent(beforeRemoveEvent); @@ -445,7 +446,7 @@ class Dashboard extends DashboardLayoutMixin( items.splice(items.indexOf(item), 1); this.items = [...this.items]; this.toggleAttribute('item-selected', false); - this.dispatchEvent(new CustomEvent('dashboard-item-removed', { detail: { item, items: this.items } })); + this.dispatchEvent(new CustomEvent('dashboard-item-removed', { detail: { item, items: this.items, section } })); } /** @private */ diff --git a/packages/dashboard/test/dashboard.test.ts b/packages/dashboard/test/dashboard.test.ts index 3b23987fb06..51377c78e76 100644 --- a/packages/dashboard/test/dashboard.test.ts +++ b/packages/dashboard/test/dashboard.test.ts @@ -115,6 +115,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' }, { id: '1' }]); + 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', () => { @@ -162,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 fbf35e49caa..e300eeb26d8 100644 --- a/packages/dashboard/test/typings/dashboard.types.ts +++ b/packages/dashboard/test/typings/dashboard.types.ts @@ -96,6 +96,7 @@ narrowedDashboard.addEventListener('dashboard-item-before-remove', (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) => { @@ -103,6 +104,7 @@ narrowedDashboard.addEventListener('dashboard-item-removed', (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) => { From 687fcaf9787826fbdadf34bccbd5a6138d62c848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Wed, 17 Dec 2025 10:56:27 +0100 Subject: [PATCH 3/3] address review comments --- packages/dashboard/src/vaadin-dashboard.js | 5 +---- packages/dashboard/test/dashboard.test.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/dashboard/src/vaadin-dashboard.js b/packages/dashboard/src/vaadin-dashboard.js index 40068548a21..b98b1dcf8c7 100644 --- a/packages/dashboard/src/vaadin-dashboard.js +++ b/packages/dashboard/src/vaadin-dashboard.js @@ -433,7 +433,7 @@ class Dashboard extends DashboardLayoutMixin( // Fire before-remove event const beforeRemoveEvent = new CustomEvent('dashboard-item-before-remove', { cancelable: true, - detail: { item, items: [...this.items], section }, + detail: { item, items: this.items, section }, }); this.dispatchEvent(beforeRemoveEvent); @@ -534,9 +534,6 @@ class Dashboard extends DashboardLayoutMixin( * Fired before an item is removed * * @event dashboard-item-before-remove - * @param {Object} detail - * @param {Object} detail.item the item to be removed - * @param {Array} detail.items the current items array */ /** diff --git a/packages/dashboard/test/dashboard.test.ts b/packages/dashboard/test/dashboard.test.ts index 51377c78e76..fd4d26aa9af 100644 --- a/packages/dashboard/test/dashboard.test.ts +++ b/packages/dashboard/test/dashboard.test.ts @@ -114,7 +114,7 @@ describe('dashboard', () => { 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' }, { 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; });