diff --git a/dev/dashboard.html b/dev/dashboard.html
index e4ad7e8223..2747ef16cb 100644
--- a/dev/dashboard.html
+++ b/dev/dashboard.html
@@ -117,6 +117,10 @@
console.log('dashboard-item-resize-end');
console.log('item after resize', e.detail);
});
+
+ dashboard.addEventListener('dashboard-item-removed', (e) => {
+ console.log('dashboard-item-removed', e.detail);
+ });
diff --git a/packages/dashboard/src/vaadin-dashboard-helpers.js b/packages/dashboard/src/vaadin-dashboard-helpers.js
new file mode 100644
index 0000000000..4c94c118bf
--- /dev/null
+++ b/packages/dashboard/src/vaadin-dashboard-helpers.js
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright (c) 2016 - 2024 Vaadin Ltd.
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
+ */
+
+export const WRAPPER_LOCAL_NAME = 'vaadin-dashboard-widget-wrapper';
+
+/**
+ * Returns the array of items that contains the given item.
+ * Might be the dashboard items or the items of a section.
+ *
+ * @param {Object} item the item element
+ * @param {Object[]} items the root level items array
+ * @return {Object[]} the items array
+ */
+export function getItemsArrayOfItem(item, items) {
+ if (items.includes(item)) {
+ return items;
+ }
+ const parentItem = items.find((i) => i.items && getItemsArrayOfItem(item, i.items));
+ return parentItem ? parentItem.items : null;
+}
+
+/**
+ * Returns the item associated with the given element.
+ *
+ * @param {HTMLElement} element the element
+ */
+export function getElementItem(element) {
+ return element.closest(WRAPPER_LOCAL_NAME).__item;
+}
diff --git a/packages/dashboard/src/vaadin-dashboard-section.js b/packages/dashboard/src/vaadin-dashboard-section.js
index ab11921faf..45c9272e7a 100644
--- a/packages/dashboard/src/vaadin-dashboard-section.js
+++ b/packages/dashboard/src/vaadin-dashboard-section.js
@@ -100,10 +100,9 @@ class DashboardSection extends ControllerMixin(ElementMixin(PolylitMixin(LitElem
render() {
return html`
@@ -135,6 +134,11 @@ class DashboardSection extends ControllerMixin(ElementMixin(PolylitMixin(LitElem
__onSectionTitleChanged(sectionTitle) {
this.__titleController.setTitle(sectionTitle);
}
+
+ /** @private */
+ __remove() {
+ this.dispatchEvent(new CustomEvent('item-remove', { bubbles: true, composed: true }));
+ }
}
defineCustomElement(DashboardSection);
diff --git a/packages/dashboard/src/vaadin-dashboard-styles.js b/packages/dashboard/src/vaadin-dashboard-styles.js
index d874cdc469..241ce9261d 100644
--- a/packages/dashboard/src/vaadin-dashboard-styles.js
+++ b/packages/dashboard/src/vaadin-dashboard-styles.js
@@ -25,13 +25,23 @@ export const dashboardWidgetAndSectionStyles = css`
align-items: center;
}
- #header-actions {
+ #drag-handle {
display: var(--_vaadin-dashboard-widget-actions-display, none);
+ font-size: 30px;
+ cursor: grab;
}
#drag-handle::before {
- font-size: 30px;
content: '☰';
- cursor: grab;
+ }
+
+ #remove-button {
+ display: var(--_vaadin-dashboard-widget-actions-display, none);
+ font-size: 30px;
+ cursor: pointer;
+ }
+
+ #remove-button::before {
+ content: '×';
}
`;
diff --git a/packages/dashboard/src/vaadin-dashboard-widget.js b/packages/dashboard/src/vaadin-dashboard-widget.js
index 3e46de5cf1..6a15e52e5e 100644
--- a/packages/dashboard/src/vaadin-dashboard-widget.js
+++ b/packages/dashboard/src/vaadin-dashboard-widget.js
@@ -52,21 +52,21 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
#resize-handle {
display: var(--_vaadin-dashboard-widget-actions-display, none);
- }
-
- #resize-handle::before {
position: absolute;
bottom: 0;
right: 0;
font-size: 30px;
- content: '\\2921';
cursor: grab;
line-height: 1;
}
+ #resize-handle::before {
+ content: '\\2921';
+ }
+
:host::after {
content: '';
- z-index: 100;
+ z-index: 2;
position: absolute;
inset-inline-start: 0;
top: 0;
@@ -96,18 +96,17 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
render() {
return html`
-
+
`;
}
@@ -153,6 +152,11 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
__updateTitle() {
this.__titleController.setTitle(this.widgetTitle);
}
+
+ /** @private */
+ __remove() {
+ this.dispatchEvent(new CustomEvent('item-remove', { bubbles: true, composed: true }));
+ }
}
defineCustomElement(DashboardWidget);
diff --git a/packages/dashboard/src/vaadin-dashboard.d.ts b/packages/dashboard/src/vaadin-dashboard.d.ts
index 5b5feb2c49..8bec317279 100644
--- a/packages/dashboard/src/vaadin-dashboard.d.ts
+++ b/packages/dashboard/src/vaadin-dashboard.d.ts
@@ -88,6 +88,15 @@ export type DashboardItemDragResizeEvent = CustomEv
rowspan: number;
}>;
+/**
+ * Fired when an item is removed
+ */
+export type DashboardItemRemoveEvent = CustomEvent<{
+ item: TItem | DashboardSectionItem;
+
+ items: Array>;
+}>;
+
export interface DashboardCustomEventMap {
'dashboard-item-reorder-start': DashboardItemReorderStartEvent;
@@ -100,6 +109,8 @@ export interface DashboardCustomEventMap {
'dashboard-item-resize-end': DashboardItemResizeEndEvent;
'dashboard-item-drag-resize': DashboardItemDragResizeEvent;
+
+ 'dashboard-item-removed': DashboardItemRemoveEvent;
}
export type DashboardEventMap = DashboardCustomEventMap & HTMLElementEventMap;
diff --git a/packages/dashboard/src/vaadin-dashboard.js b/packages/dashboard/src/vaadin-dashboard.js
index 8055a13cf2..524b0641f7 100644
--- a/packages/dashboard/src/vaadin-dashboard.js
+++ b/packages/dashboard/src/vaadin-dashboard.js
@@ -16,6 +16,7 @@ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { css, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
+import { getElementItem, getItemsArrayOfItem } from './vaadin-dashboard-helpers.js';
import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js';
import { hasWidgetWrappers } from './vaadin-dashboard-styles.js';
import { WidgetReorderController } from './widget-reorder-controller.js';
@@ -30,6 +31,7 @@ import { WidgetResizeController } from './widget-resize-controller.js';
* @fires {CustomEvent} dashboard-item-drag-resize - Fired when an item will be resized by dragging
* @fires {CustomEvent} dashboard-item-resize-start - Fired when item resizing starts
* @fires {CustomEvent} dashboard-item-resize-end - Fired when item resizing ends
+ * @fires {CustomEvent} dashboard-item-removed - Fired when an item is removed
*
* @customElement
* @extends HTMLElement
@@ -108,6 +110,7 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab
super();
this.__widgetReorderController = new WidgetReorderController(this);
this.__widgetResizeController = new WidgetResizeController(this);
+ this.addEventListener('item-remove', (e) => this.__itemRemove(e));
}
/** @protected */
@@ -176,6 +179,18 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab
});
}
+ /** @private */
+ __itemRemove(e) {
+ e.stopImmediatePropagation();
+ const item = getElementItem(e.target);
+ const items = getItemsArrayOfItem(item, this.items);
+ items.splice(items.indexOf(item), 1);
+ this.items = [...this.items];
+ this.dispatchEvent(
+ new CustomEvent('dashboard-item-removed', { cancelable: true, detail: { item, items: this.items } }),
+ );
+ }
+
/**
* Fired when item reordering starts
*
@@ -211,6 +226,12 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab
*
* @event dashboard-item-drag-resize
*/
+
+ /**
+ * Fired when an item is removed
+ *
+ * @event dashboard-item-removed
+ */
}
defineCustomElement(Dashboard);
diff --git a/packages/dashboard/src/widget-reorder-controller.js b/packages/dashboard/src/widget-reorder-controller.js
index 0fe154244c..e7f1137d79 100644
--- a/packages/dashboard/src/widget-reorder-controller.js
+++ b/packages/dashboard/src/widget-reorder-controller.js
@@ -4,7 +4,8 @@
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
-const WRAPPER_LOCAL_NAME = 'vaadin-dashboard-widget-wrapper';
+import { getElementItem, getItemsArrayOfItem, WRAPPER_LOCAL_NAME } from './vaadin-dashboard-helpers.js';
+
const REORDER_EVENT_TIMEOUT = 200;
/**
@@ -30,7 +31,7 @@ export class WidgetReorderController extends EventTarget {
}
this.__draggedElement = e.target;
- this.draggedItem = this.__getElementItem(this.__draggedElement);
+ this.draggedItem = getElementItem(this.__draggedElement);
// Set the drag image to the dragged element
const { left, top } = this.__draggedElement.getBoundingClientRect();
@@ -61,7 +62,7 @@ export class WidgetReorderController extends EventTarget {
// Get all elements that are candidates for reordering with the dragged element
const dragContextElements = this.__getDragContextElements(this.__draggedElement);
// Find the up-to-date element instance representing the dragged item
- const draggedElement = dragContextElements.find((element) => this.__getElementItem(element) === this.draggedItem);
+ const draggedElement = dragContextElements.find((element) => getElementItem(element) === this.draggedItem);
if (!draggedElement) {
return;
}
@@ -82,8 +83,8 @@ export class WidgetReorderController extends EventTarget {
this.__reordering = false;
}, REORDER_EVENT_TIMEOUT);
- const targetItem = this.__getElementItem(closestElement);
- const targetItems = this.__getItemsArrayOfItem(targetItem);
+ const targetItem = getElementItem(closestElement);
+ const targetItems = getItemsArrayOfItem(targetItem, this.host.items);
const targetIndex = targetItems.indexOf(targetItem);
const reorderEvent = new CustomEvent('dashboard-item-drag-reorder', {
@@ -172,11 +173,6 @@ export class WidgetReorderController extends EventTarget {
}
}
- /** @private */
- __getElementItem(element) {
- return element.closest(WRAPPER_LOCAL_NAME).__item;
- }
-
/**
* Returns the elements (widgets or sections) that are candidates for reordering with the
* currently dragged item. Effectively, this is the list of child widgets or sections inside
@@ -200,33 +196,13 @@ export class WidgetReorderController extends EventTarget {
/** @private */
__reorderItems(draggedItem, targetIndex) {
- const items = this.__getItemsArrayOfItem(draggedItem);
+ const items = getItemsArrayOfItem(draggedItem, this.host.items);
const draggedIndex = items.indexOf(draggedItem);
items.splice(draggedIndex, 1);
items.splice(targetIndex, 0, draggedItem);
this.host.items = [...this.host.items];
}
- /**
- * Returns the array of items that contains the given item.
- * Might be the host items or the items of a section.
- * @private
- */
- __getItemsArrayOfItem(item, items = this.host.items) {
- for (const i of items) {
- if (i === item) {
- return items;
- }
- if (i.items) {
- const result = this.__getItemsArrayOfItem(item, i.items);
- if (result) {
- return result;
- }
- }
- }
- return null;
- }
-
/**
* The dragged element might be removed from the DOM during the drag operation if
* the widgets get re-rendered. This method restores the dragged element if it's not
diff --git a/packages/dashboard/src/widget-resize-controller.js b/packages/dashboard/src/widget-resize-controller.js
index b2f59eaf38..a720a83e2a 100644
--- a/packages/dashboard/src/widget-resize-controller.js
+++ b/packages/dashboard/src/widget-resize-controller.js
@@ -4,8 +4,8 @@
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
-const WRAPPER_LOCAL_NAME = 'vaadin-dashboard-widget-wrapper';
import { addListener } from '@vaadin/component-base/src/gestures.js';
+import { getElementItem, WRAPPER_LOCAL_NAME } from './vaadin-dashboard-helpers.js';
/**
* A controller to widget resizing inside a dashboard.
@@ -38,7 +38,7 @@ export class WidgetResizeController extends EventTarget {
}
this.host.$.grid.toggleAttribute('resizing', true);
- this.resizedItem = this.__getElementItem(e.target);
+ this.resizedItem = getElementItem(e.target);
this.__resizeStartWidth = e.target.offsetWidth;
this.__resizeStartHeight = e.target.offsetHeight;
@@ -133,11 +133,6 @@ export class WidgetResizeController extends EventTarget {
this.resizedItem = null;
}
- /** @private */
- __getElementItem(element) {
- return element.closest(WRAPPER_LOCAL_NAME).__item;
- }
-
/** @private */
__getItemWrapper(item) {
return [...this.host.querySelectorAll(WRAPPER_LOCAL_NAME)].find((el) => el.__item === item);
diff --git a/packages/dashboard/test/dashboard-widget-reordering.test.ts b/packages/dashboard/test/dashboard-widget-reordering.test.ts
index c9c5ade5ee..d9519cbcfc 100644
--- a/packages/dashboard/test/dashboard-widget-reordering.test.ts
+++ b/packages/dashboard/test/dashboard-widget-reordering.test.ts
@@ -23,7 +23,7 @@ type TestDashboardItem = DashboardItem & { id: number };
describe('dashboard - widget reordering', () => {
let dashboard: Dashboard;
- const columnWidth = 100;
+ const columnWidth = 200;
beforeEach(async () => {
dashboard = fixtureSync('');
diff --git a/packages/dashboard/test/dashboard-widget-resizing.test.ts b/packages/dashboard/test/dashboard-widget-resizing.test.ts
index 18e630e9d3..28bb344a49 100644
--- a/packages/dashboard/test/dashboard-widget-resizing.test.ts
+++ b/packages/dashboard/test/dashboard-widget-resizing.test.ts
@@ -20,7 +20,7 @@ type TestDashboardItem = DashboardItem & { id: number };
describe('dashboard - widget resizing', () => {
let dashboard: Dashboard;
- const columnWidth = 100;
+ const columnWidth = 200;
const rowHeight = 100;
beforeEach(async () => {
@@ -60,6 +60,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 0, 1)!, 'end');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// Expect the widgets to be reordered
// prettier-ignore
expectLayout(dashboard, [
@@ -75,6 +78,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 0, 1)!, 'start');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
@@ -96,6 +102,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 0, 0)!, 'start');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
@@ -117,6 +126,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 0, 0)!, 'end');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 0],
@@ -139,6 +151,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 1, 0)!, 'bottom');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
@@ -161,6 +176,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 1, 0)!, 'top');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
@@ -183,6 +201,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 0, 0)!, 'top');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
@@ -205,6 +226,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 0, 0)!, 'bottom');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
@@ -228,6 +252,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 1, 0)!, 'bottom');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
@@ -247,6 +274,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 0, 1)!, 'end');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
@@ -274,6 +304,9 @@ describe('dashboard - widget resizing', () => {
fireResizeOver(getElementFromCell(dashboard, 0, 1)!, 'end');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
expect(resizeSpy).to.have.been.calledOnce;
expect(resizeSpy.getCall(0).args[0].detail).to.deep.equal({
item: { id: 0 },
@@ -288,6 +321,9 @@ describe('dashboard - widget resizing', () => {
await nextFrame();
fireResizeOver(getElementFromCell(dashboard, 0, 1)!, 'end');
await nextFrame();
+ fireResizeEnd(dashboard);
+ await nextFrame();
+
// prettier-ignore
expectLayout(dashboard, [
[0, 1],
diff --git a/packages/dashboard/test/dashboard.test.ts b/packages/dashboard/test/dashboard.test.ts
index a24bdd79f0..65c1ea4752 100644
--- a/packages/dashboard/test/dashboard.test.ts
+++ b/packages/dashboard/test/dashboard.test.ts
@@ -3,8 +3,16 @@ import { fixtureSync, nextFrame } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import '../vaadin-dashboard.js';
import type { CustomElementType } from '@vaadin/component-base/src/define.js';
+import type { DashboardWidget } from '../src/vaadin-dashboard-widget.js';
import type { Dashboard, DashboardItem } from '../vaadin-dashboard.js';
-import { getDraggable, getElementFromCell, setGap, setMaximumColumnWidth, setMinimumColumnWidth } from './helpers.js';
+import {
+ getDraggable,
+ getElementFromCell,
+ getRemoveButton,
+ setGap,
+ setMaximumColumnWidth,
+ setMinimumColumnWidth,
+} from './helpers.js';
type TestDashboardItem = DashboardItem & { id: string; component?: Element | string };
@@ -79,6 +87,31 @@ describe('dashboard', () => {
expect(dashboard.querySelectorAll('vaadin-dashboard-widget')).to.be.empty;
});
+ it('should remove a widget', () => {
+ const widget = getElementFromCell(dashboard, 0, 1);
+ getRemoveButton(widget as DashboardWidget).click();
+ expect(dashboard.items).to.eql([{ id: 'Item 0' }]);
+ });
+
+ it('should dispatch an dashboard-item-removed event', () => {
+ const spy = sinon.spy();
+ dashboard.addEventListener('dashboard-item-removed', 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: 'Item 1' });
+ expect(spy.firstCall.args[0].detail.items).to.eql([{ id: 'Item 0' }]);
+ });
+
+ it('should not dispatch an item-remove event', () => {
+ const spy = sinon.spy();
+ // @ts-ignore unexpected event type
+ dashboard.addEventListener('item-remove', spy);
+ const widget = getElementFromCell(dashboard, 0, 1);
+ getRemoveButton(widget as DashboardWidget).click();
+ expect(spy).to.not.be.called;
+ });
+
describe('custom element definition', () => {
let tagName: string;
@@ -163,6 +196,13 @@ describe('dashboard', () => {
expect(widget3?.localName).to.equal('vaadin-dashboard-widget');
expect(widget3).to.have.property('widgetTitle', 'Item 3 title');
});
+
+ it('should remove a section', () => {
+ const widget = getElementFromCell(dashboard, 1, 0);
+ const section = widget?.closest('vaadin-dashboard-section');
+ getRemoveButton(section!).click();
+ expect(dashboard.items).to.eql([{ id: 'Item 0' }, { id: 'Item 1' }]);
+ });
});
describe('editable', () => {
diff --git a/packages/dashboard/test/helpers.ts b/packages/dashboard/test/helpers.ts
index 69c44ff011..a3c59c48ba 100644
--- a/packages/dashboard/test/helpers.ts
+++ b/packages/dashboard/test/helpers.ts
@@ -1,5 +1,7 @@
import { expect } from '@vaadin/chai-plugins';
import sinon from 'sinon';
+import type { DashboardSection } from '../src/vaadin-dashboard-section.js';
+import type { DashboardWidget } from '../src/vaadin-dashboard-widget.js';
function getCssGrid(element: Element): Element {
return (element as any).$?.grid || element;
@@ -273,3 +275,7 @@ export function fireResizeStart(resizedWidget: Element): void {
export function fireResizeEnd(dragOverTarget: Element): void {
dragOverTarget.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, composed: true }));
}
+
+export function getRemoveButton(section: DashboardWidget | DashboardSection): HTMLElement {
+ return section.shadowRoot!.querySelector('#remove-button') as HTMLElement;
+}
diff --git a/packages/dashboard/test/typings/dashboard.types.ts b/packages/dashboard/test/typings/dashboard.types.ts
index df2601a3b2..f7aab45673 100644
--- a/packages/dashboard/test/typings/dashboard.types.ts
+++ b/packages/dashboard/test/typings/dashboard.types.ts
@@ -6,6 +6,7 @@ import type {
DashboardItem,
DashboardItemDragReorderEvent,
DashboardItemDragResizeEvent,
+ DashboardItemRemoveEvent,
DashboardItemReorderEndEvent,
DashboardItemReorderStartEvent,
DashboardItemResizeEndEvent,
@@ -77,6 +78,11 @@ narrowedDashboard.addEventListener('dashboard-item-drag-resize', (event) => {
assertType(event.detail.rowspan);
});
+narrowedDashboard.addEventListener('dashboard-item-removed', (event) => {
+ assertType>(event);
+ assertType>(event.detail.item);
+});
+
/* DashboardLayout */
const layout = document.createElement('vaadin-dashboard-layout');
assertType(layout);