diff --git a/dev/dashboard.html b/dev/dashboard.html index 10f4c8bddc..81f1c6cae7 100644 --- a/dev/dashboard.html +++ b/dev/dashboard.html @@ -102,6 +102,20 @@ console.log('dashboard-item-reorder-end'); console.log('items after reorder', e.target.items); }); + + dashboard.addEventListener('dashboard-item-resize-start', (e) => { + console.log('dashboard-item-resize-start'); + }); + + dashboard.addEventListener('dashboard-item-drag-resize', (e) => { + console.log('dashboard-item-drag-resize', e.detail); + // e.preventDefault(); + }); + + dashboard.addEventListener('dashboard-item-resize-end', (e) => { + console.log('dashboard-item-resize-end'); + console.log('item after resize', e.detail); + }); diff --git a/packages/dashboard/src/vaadin-dashboard.d.ts b/packages/dashboard/src/vaadin-dashboard.d.ts index 9dac015b4b..5b5feb2c49 100644 --- a/packages/dashboard/src/vaadin-dashboard.d.ts +++ b/packages/dashboard/src/vaadin-dashboard.d.ts @@ -65,12 +65,41 @@ export type DashboardItemDragReorderEvent = CustomE targetIndex: number; }>; +/** + * Fired when item resizing starts + */ +export type DashboardItemResizeStartEvent = CustomEvent<{ + item: TItem; +}>; + +/** + * Fired when item resizing ends + */ +export type DashboardItemResizeEndEvent = CustomEvent<{ + item: TItem; +}>; + +/** + * Fired when an item will be resized by dragging + */ +export type DashboardItemDragResizeEvent = CustomEvent<{ + item: TItem; + colspan: number; + rowspan: number; +}>; + export interface DashboardCustomEventMap { 'dashboard-item-reorder-start': DashboardItemReorderStartEvent; 'dashboard-item-reorder-end': DashboardItemReorderEndEvent; 'dashboard-item-drag-reorder': DashboardItemDragReorderEvent; + + 'dashboard-item-resize-start': DashboardItemResizeStartEvent; + + 'dashboard-item-resize-end': DashboardItemResizeEndEvent; + + 'dashboard-item-drag-resize': DashboardItemDragResizeEvent; } export type DashboardEventMap = DashboardCustomEventMap & HTMLElementEventMap; diff --git a/packages/dashboard/src/vaadin-dashboard.js b/packages/dashboard/src/vaadin-dashboard.js index e065a64738..ea88afe402 100644 --- a/packages/dashboard/src/vaadin-dashboard.js +++ b/packages/dashboard/src/vaadin-dashboard.js @@ -27,6 +27,9 @@ import { WidgetResizeController } from './widget-resize-controller.js'; * @fires {CustomEvent} dashboard-item-drag-reorder - Fired when an items will be reordered by dragging * @fires {CustomEvent} dashboard-item-reorder-start - Fired when item reordering starts * @fires {CustomEvent} dashboard-item-reorder-end - Fired when item reordering ends + * @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 * * @customElement * @extends HTMLElement @@ -106,6 +109,12 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab this.__widgetResizeController = new WidgetResizeController(this); } + /** @protected */ + disconnectedCallback() { + super.disconnectedCallback(); + this.__widgetResizeController.cleanup(); + } + /** @protected */ ready() { super.ready(); @@ -189,6 +198,24 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab * * @event dashboard-item-drag-reorder */ + + /** + * Fired when item resizing starts + * + * @event dashboard-item-resize-start + */ + + /** + * Fired when item resizing ends + * + * @event dashboard-item-resize-end + */ + + /** + * Fired when an item will be resized by dragging + * + * @event dashboard-item-drag-resize + */ } defineCustomElement(Dashboard); diff --git a/packages/dashboard/src/widget-resize-controller.js b/packages/dashboard/src/widget-resize-controller.js index 6734ad4828..dadec05b5d 100644 --- a/packages/dashboard/src/widget-resize-controller.js +++ b/packages/dashboard/src/widget-resize-controller.js @@ -110,7 +110,7 @@ export class WidgetResizeController extends EventTarget { this.host.$.grid.toggleAttribute('resizing', false); this.__resizedElementRemoveObserver.disconnect(); - document.removeEventListener('touchmove', this.__touchMoveCancelListener); + this.cleanup(); this.host.dispatchEvent(new CustomEvent('dashboard-item-resize-end')); } @@ -126,14 +126,25 @@ export class WidgetResizeController extends EventTarget { } /** @private */ - __updateResizedItem(colspan, rowspan) { - if (this.resizedItem.colspan !== colspan || this.resizedItem.rowspan !== rowspan) { - this.resizedItem.colspan = colspan; - this.resizedItem.rowspan = rowspan; - this.host.dispatchEvent(new CustomEvent('dashboard-item-resize', { detail: { item: this.resizedItem } })); - this.host.items = [...this.host.items]; - requestAnimationFrame(() => this.__updateWidgetStyles()); + __updateResizedItem(colspan = 1, rowspan = 1) { + if (this.resizedItem.colspan === colspan && this.resizedItem.rowspan === rowspan) { + return; + } + + const resizeEvent = new CustomEvent('dashboard-item-drag-resize', { + detail: { item: this.resizedItem, colspan, rowspan }, + cancelable: true, + }); + + // Dispatch the resize event and resize items if the event is not canceled + if (!this.host.dispatchEvent(resizeEvent)) { + return; } + + this.resizedItem.colspan = colspan; + this.resizedItem.rowspan = rowspan; + this.host.items = [...this.host.items]; + requestAnimationFrame(() => this.__updateWidgetStyles()); } /** @private */ @@ -150,4 +161,8 @@ export class WidgetResizeController extends EventTarget { this.host.appendChild(this.__resizedElement); } } + + cleanup() { + document.removeEventListener('touchmove', this.__touchMoveCancelListener); + } } diff --git a/packages/dashboard/test/dashboard-widget-resizing.test.ts b/packages/dashboard/test/dashboard-widget-resizing.test.ts index a8d7cee3f0..a5e79f8bee 100644 --- a/packages/dashboard/test/dashboard-widget-resizing.test.ts +++ b/packages/dashboard/test/dashboard-widget-resizing.test.ts @@ -1,5 +1,6 @@ import { expect } from '@vaadin/chai-plugins'; import { fixtureSync, nextFrame } from '@vaadin/testing-helpers'; +import sinon from 'sinon'; import '../vaadin-dashboard.js'; import type { Dashboard, DashboardItem } from '../vaadin-dashboard.js'; import { @@ -228,6 +229,75 @@ describe('dashboard - widget resizing', () => { ]); }); + it('should dispatch an item resize start event', async () => { + const resizeStartSpy = sinon.spy(); + dashboard.addEventListener('dashboard-item-resize-start', resizeStartSpy); + fireResizeStart(getElementFromCell(dashboard, 0, 0)!); + await nextFrame(); + + expect(resizeStartSpy).to.have.been.calledOnce; + }); + + it('should dispatch an item drag resize event', async () => { + const resizeSpy = sinon.spy(); + dashboard.addEventListener('dashboard-item-drag-resize', resizeSpy); + dashboard.addEventListener('dashboard-item-drag-resize', (e) => e.preventDefault()); + fireResizeStart(getElementFromCell(dashboard, 0, 0)!); + await nextFrame(); + fireResizeOver(getElementFromCell(dashboard, 0, 1)!, 'end'); + await nextFrame(); + + expect(resizeSpy).to.have.been.calledOnce; + expect(resizeSpy.getCall(0).args[0].detail).to.deep.equal({ + item: { id: 0 }, + colspan: 2, + rowspan: 1, + }); + }); + + it('should not resize if the drag resize event is cancelled', async () => { + dashboard.addEventListener('dashboard-item-drag-resize', (e) => e.preventDefault()); + fireResizeStart(getElementFromCell(dashboard, 0, 0)!); + await nextFrame(); + fireResizeOver(getElementFromCell(dashboard, 0, 1)!, 'end'); + await nextFrame(); + // prettier-ignore + expectLayout(dashboard, [ + [0, 1], + ]); + }); + + it('should dispatch an item resize end event', async () => { + const resizeEndSpy = sinon.spy(); + dashboard.addEventListener('dashboard-item-resize-end', resizeEndSpy); + fireResizeStart(getElementFromCell(dashboard, 0, 0)!); + await nextFrame(); + fireResizeEnd(dashboard); + await nextFrame(); + + expect(resizeEndSpy).to.have.been.calledOnce; + }); + + it('should cancel touchmove events while resizing', async () => { + fireResizeStart(getElementFromCell(dashboard, 0, 0)!); + await nextFrame(); + const touchmove = new TouchEvent('touchmove', { cancelable: true, bubbles: true }); + document.dispatchEvent(touchmove); + + expect(touchmove.defaultPrevented).to.be.true; + }); + + it('should not cancel touchmove events after resizing has finished', async () => { + fireResizeStart(getElementFromCell(dashboard, 0, 0)!); + await nextFrame(); + fireResizeEnd(dashboard); + + const touchmove = new TouchEvent('touchmove', { cancelable: true, bubbles: true }); + document.dispatchEvent(touchmove); + + expect(touchmove.defaultPrevented).to.be.false; + }); + // Make sure the original resized element is restored in the host. // Otherwise, "track" event would stop working. describe('ensure track event', () => { diff --git a/packages/dashboard/test/typings/dashboard.types.ts b/packages/dashboard/test/typings/dashboard.types.ts index 0489bebbb5..c16507f5c8 100644 --- a/packages/dashboard/test/typings/dashboard.types.ts +++ b/packages/dashboard/test/typings/dashboard.types.ts @@ -5,8 +5,11 @@ import type { Dashboard, DashboardItem, DashboardItemDragReorderEvent, + DashboardItemDragResizeEvent, DashboardItemReorderEndEvent, DashboardItemReorderStartEvent, + DashboardItemResizeEndEvent, + DashboardItemResizeStartEvent, DashboardRenderer, DashboardSectionItem, } from '../../vaadin-dashboard.js'; @@ -57,6 +60,22 @@ narrowedDashboard.addEventListener('dashboard-item-drag-reorder', (event) => { assertType(event.detail.targetIndex); }); +narrowedDashboard.addEventListener('dashboard-item-resize-start', (event) => { + assertType>(event); +}); + +narrowedDashboard.addEventListener('dashboard-item-resize-end', (event) => { + assertType>(event); + assertType(event.detail.item); +}); + +narrowedDashboard.addEventListener('dashboard-item-drag-resize', (event) => { + assertType>(event); + assertType(event.detail.item); + assertType(event.detail.colspan); + assertType(event.detail.rowspan); +}); + /* DashboardLayout */ const layout = document.createElement('vaadin-dashboard-layout'); assertType(layout);