Skip to content

Commit 1b75f31

Browse files
authored
feat: allow preventing automatic item removal in Dashboard (#10623)
* implement before remove event * add section to event * address review comments
1 parent 1a09d1e commit 1b75f31

File tree

4 files changed

+137
-3
lines changed

4 files changed

+137
-3
lines changed

packages/dashboard/src/vaadin-dashboard.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,26 @@ export type DashboardItemResizedEvent<TItem extends DashboardItem> = CustomEvent
8585
items: Array<TItem | DashboardSectionItem<TItem>>;
8686
}>;
8787

88+
/**
89+
* Fired before an item is removed. Calling preventDefault() on the event will cancel the removal.
90+
*/
91+
export type DashboardItemBeforeRemoveEvent<TItem extends DashboardItem> = CustomEvent<{
92+
item: TItem | DashboardSectionItem<TItem>;
93+
94+
items: Array<TItem | DashboardSectionItem<TItem>>;
95+
96+
section: DashboardSectionItem<TItem> | undefined;
97+
}>;
98+
8899
/**
89100
* Fired when an item was removed
90101
*/
91102
export type DashboardItemRemovedEvent<TItem extends DashboardItem> = CustomEvent<{
92103
item: TItem | DashboardSectionItem<TItem>;
93104

94105
items: Array<TItem | DashboardSectionItem<TItem>>;
106+
107+
section: DashboardSectionItem<TItem> | undefined;
95108
}>;
96109

97110
/**
@@ -123,6 +136,8 @@ export interface DashboardCustomEventMap<TItem extends DashboardItem> {
123136

124137
'dashboard-item-resized': DashboardItemResizedEvent<TItem>;
125138

139+
'dashboard-item-before-remove': DashboardItemBeforeRemoveEvent<TItem>;
140+
126141
'dashboard-item-removed': DashboardItemRemovedEvent<TItem>;
127142

128143
'dashboard-item-selected-changed': DashboardItemSelectedChangedEvent<TItem>;

packages/dashboard/src/vaadin-dashboard.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import { WidgetResizeController } from './widget-resize-controller.js';
9393
*
9494
* @fires {CustomEvent} dashboard-item-moved - Fired when an item was moved
9595
* @fires {CustomEvent} dashboard-item-resized - Fired when an item was resized
96+
* @fires {CustomEvent} dashboard-item-before-remove - Fired before an item is removed. Calling preventDefault() on the event will cancel the removal.
9697
* @fires {CustomEvent} dashboard-item-removed - Fired when an item was removed
9798
* @fires {CustomEvent} dashboard-item-selected-changed - Fired when an item selected state changed
9899
* @fires {CustomEvent} dashboard-item-move-mode-changed - Fired when an item move mode changed
@@ -427,12 +428,25 @@ class Dashboard extends DashboardLayoutMixin(
427428
e.stopImmediatePropagation();
428429
const item = getElementItem(e.target);
429430
const items = getItemsArrayOfItem(item, this.items);
431+
const section = this.items.find((i) => i.items && i.items.includes(item));
432+
433+
// Fire before-remove event
434+
const beforeRemoveEvent = new CustomEvent('dashboard-item-before-remove', {
435+
cancelable: true,
436+
detail: { item, items: this.items, section },
437+
});
438+
this.dispatchEvent(beforeRemoveEvent);
439+
440+
// Check if removal was prevented
441+
if (beforeRemoveEvent.defaultPrevented) {
442+
return;
443+
}
444+
445+
// Proceed with removal
430446
items.splice(items.indexOf(item), 1);
431447
this.items = [...this.items];
432448
this.toggleAttribute('item-selected', false);
433-
this.dispatchEvent(
434-
new CustomEvent('dashboard-item-removed', { cancelable: true, detail: { item, items: this.items } }),
435-
);
449+
this.dispatchEvent(new CustomEvent('dashboard-item-removed', { detail: { item, items: this.items, section } }));
436450
}
437451

438452
/** @private */
@@ -516,6 +530,12 @@ class Dashboard extends DashboardLayoutMixin(
516530
* @event dashboard-item-resized
517531
*/
518532

533+
/**
534+
* Fired before an item is removed
535+
*
536+
* @event dashboard-item-before-remove
537+
*/
538+
519539
/**
520540
* Fired when an item was removed
521541
*

packages/dashboard/test/dashboard.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,74 @@ describe('dashboard', () => {
107107
expect(dashboard.items).to.eql([{ id: '0' }]);
108108
});
109109

110+
it('should dispatch a dashboard-item-before-remove event before removal', () => {
111+
const spy = sinon.spy();
112+
dashboard.addEventListener('dashboard-item-before-remove', spy);
113+
const widget = getElementFromCell(dashboard, 0, 1);
114+
getRemoveButton(widget as DashboardWidget).click();
115+
expect(spy).to.be.calledOnce;
116+
expect(spy.firstCall.args[0].detail.item).to.eql({ id: '1' });
117+
expect(spy.firstCall.args[0].detail.items).to.eql([{ id: '0' }]); // contains the state after removal
118+
expect(spy.firstCall.args[0].detail.section).to.be.undefined;
119+
});
120+
121+
it('should include section in dashboard-item-before-remove event for nested items', async () => {
122+
const sectionItem: DashboardSectionItem<TestDashboardItem> = {
123+
title: 'Section',
124+
items: [{ id: '2' }, { id: '3' }],
125+
};
126+
dashboard.items = [{ id: '0' }, { id: '1' }, sectionItem];
127+
await updateComplete(dashboard);
128+
129+
const spy = sinon.spy();
130+
dashboard.addEventListener('dashboard-item-before-remove', spy);
131+
const widget = getElementFromCell(dashboard, 1, 0);
132+
const section = widget?.closest('vaadin-dashboard-section');
133+
expect(section).to.be.ok;
134+
getRemoveButton(widget as DashboardWidget).click();
135+
136+
expect(spy).to.be.calledOnce;
137+
expect(spy.firstCall.args[0].detail.item).to.eql({ id: '2' });
138+
expect(spy.firstCall.args[0].detail.section).to.equal(sectionItem);
139+
});
140+
141+
it('should cancel removal when preventDefault is called on dashboard-item-before-remove', () => {
142+
const originalItems = dashboard.items;
143+
dashboard.addEventListener('dashboard-item-before-remove', (e) => {
144+
e.preventDefault();
145+
});
146+
const widget = getElementFromCell(dashboard, 0, 1);
147+
getRemoveButton(widget as DashboardWidget).click();
148+
expect(dashboard.items).to.equal(originalItems);
149+
expect(dashboard.items).to.eql([{ id: '0' }, { id: '1' }]);
150+
});
151+
152+
it('should not fire dashboard-item-removed when dashboard-item-before-remove is prevented', () => {
153+
const beforeRemoveSpy = sinon.spy();
154+
const removedSpy = sinon.spy();
155+
dashboard.addEventListener('dashboard-item-before-remove', (e) => {
156+
beforeRemoveSpy();
157+
e.preventDefault();
158+
});
159+
dashboard.addEventListener('dashboard-item-removed', removedSpy);
160+
const widget = getElementFromCell(dashboard, 0, 1);
161+
getRemoveButton(widget as DashboardWidget).click();
162+
expect(beforeRemoveSpy).to.be.calledOnce;
163+
expect(removedSpy).to.not.be.called;
164+
});
165+
166+
it('should fire both events in correct order when not prevented', () => {
167+
const beforeRemoveSpy = sinon.spy();
168+
const removedSpy = sinon.spy();
169+
dashboard.addEventListener('dashboard-item-before-remove', beforeRemoveSpy);
170+
dashboard.addEventListener('dashboard-item-removed', removedSpy);
171+
const widget = getElementFromCell(dashboard, 0, 1);
172+
getRemoveButton(widget as DashboardWidget).click();
173+
expect(beforeRemoveSpy).to.be.calledOnce;
174+
expect(removedSpy).to.be.calledOnce;
175+
expect(beforeRemoveSpy).to.have.been.calledBefore(removedSpy);
176+
});
177+
110178
it('should dispatch an dashboard-item-removed event', () => {
111179
const spy = sinon.spy();
112180
dashboard.addEventListener('dashboard-item-removed', spy);
@@ -115,6 +183,27 @@ describe('dashboard', () => {
115183
expect(spy).to.be.calledOnce;
116184
expect(spy.firstCall.args[0].detail.item).to.eql({ id: '1' });
117185
expect(spy.firstCall.args[0].detail.items).to.eql([{ id: '0' }]);
186+
expect(spy.firstCall.args[0].detail.section).to.be.undefined;
187+
});
188+
189+
it('should include section in dashboard-item-removed event for nested items', async () => {
190+
const sectionItem: DashboardSectionItem<TestDashboardItem> = {
191+
title: 'Section',
192+
items: [{ id: '2' }, { id: '3' }],
193+
};
194+
dashboard.items = [{ id: '0' }, { id: '1' }, sectionItem];
195+
await updateComplete(dashboard);
196+
197+
const spy = sinon.spy();
198+
dashboard.addEventListener('dashboard-item-removed', spy);
199+
const widget = getElementFromCell(dashboard, 1, 0);
200+
const section = widget?.closest('vaadin-dashboard-section');
201+
expect(section).to.be.ok;
202+
getRemoveButton(widget as DashboardWidget).click();
203+
204+
expect(spy).to.be.calledOnce;
205+
expect(spy.firstCall.args[0].detail.item).to.eql({ id: '2' });
206+
expect(spy.firstCall.args[0].detail.section).to.equal(sectionItem);
118207
});
119208

120209
it('should not dispatch an item-remove event', async () => {

packages/dashboard/test/typings/dashboard.types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
Dashboard,
66
DashboardI18n,
77
DashboardItem,
8+
DashboardItemBeforeRemoveEvent,
89
DashboardItemMovedEvent,
910
DashboardItemMoveModeChangedEvent,
1011
DashboardItemRemovedEvent,
@@ -90,11 +91,20 @@ narrowedDashboard.addEventListener('dashboard-item-resized', (event) => {
9091
assertType<Array<TestDashboardItem | DashboardSectionItem<TestDashboardItem>>>(event.detail.items);
9192
});
9293

94+
narrowedDashboard.addEventListener('dashboard-item-before-remove', (event) => {
95+
assertType<DashboardItemBeforeRemoveEvent<TestDashboardItem>>(event);
96+
assertType<TestDashboardItem>(event.detail.item as TestDashboardItem);
97+
assertType<DashboardSectionItem<TestDashboardItem>>(event.detail.item as DashboardSectionItem<TestDashboardItem>);
98+
assertType<Array<TestDashboardItem | DashboardSectionItem<TestDashboardItem>>>(event.detail.items);
99+
assertType<DashboardSectionItem<TestDashboardItem> | undefined>(event.detail.section);
100+
});
101+
93102
narrowedDashboard.addEventListener('dashboard-item-removed', (event) => {
94103
assertType<DashboardItemRemovedEvent<TestDashboardItem>>(event);
95104
assertType<TestDashboardItem>(event.detail.item as TestDashboardItem);
96105
assertType<DashboardSectionItem<TestDashboardItem>>(event.detail.item as DashboardSectionItem<TestDashboardItem>);
97106
assertType<Array<TestDashboardItem | DashboardSectionItem<TestDashboardItem>>>(event.detail.items);
107+
assertType<DashboardSectionItem<TestDashboardItem> | undefined>(event.detail.section);
98108
});
99109

100110
narrowedDashboard.addEventListener('dashboard-item-selected-changed', (event) => {

0 commit comments

Comments
 (0)