From f7051b0c1a882fbd098b8b694685d45f20ec3e8b Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Wed, 21 Aug 2024 14:58:00 +0300 Subject: [PATCH] feat: add items and renderer to dashboard (#7680) --- dev/dashboard.html | 81 +++++++++++++++++ .../src/vaadin-dashboard-layout-mixin.js | 4 +- .../dashboard/src/vaadin-dashboard-widget.js | 1 + packages/dashboard/src/vaadin-dashboard.d.ts | 41 ++++++++- packages/dashboard/src/vaadin-dashboard.js | 75 ++++++++++++++- packages/dashboard/test/dashboard.test.ts | 91 ++++++++++++++++++- packages/dashboard/test/helpers.ts | 5 +- .../dashboard/test/typings/dashboard.types.ts | 21 ++++- 8 files changed, 303 insertions(+), 16 deletions(-) diff --git a/dev/dashboard.html b/dev/dashboard.html index b09663600a..70317c1402 100644 --- a/dev/dashboard.html +++ b/dev/dashboard.html @@ -7,8 +7,89 @@ Dashboard + + diff --git a/packages/dashboard/src/vaadin-dashboard-layout-mixin.js b/packages/dashboard/src/vaadin-dashboard-layout-mixin.js index 5242243f07..5d3c819b87 100644 --- a/packages/dashboard/src/vaadin-dashboard-layout-mixin.js +++ b/packages/dashboard/src/vaadin-dashboard-layout-mixin.js @@ -58,11 +58,13 @@ export const DashboardLayoutMixin = (superClass) => } ::slotted(*) { - grid-column: span + --_vaadin-dashboard-item-column: span min( var(--vaadin-dashboard-item-colspan, 1), var(--_vaadin-dashboard-effective-col-count, var(--_vaadin-dashboard-col-count)) ); + + grid-column: var(--_vaadin-dashboard-item-column); } `; } diff --git a/packages/dashboard/src/vaadin-dashboard-widget.js b/packages/dashboard/src/vaadin-dashboard-widget.js index 5cb370802f..0b2aab613f 100644 --- a/packages/dashboard/src/vaadin-dashboard-widget.js +++ b/packages/dashboard/src/vaadin-dashboard-widget.js @@ -34,6 +34,7 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme :host { display: flex; flex-direction: column; + grid-column: var(--_vaadin-dashboard-item-column); } :host([hidden]) { diff --git a/packages/dashboard/src/vaadin-dashboard.d.ts b/packages/dashboard/src/vaadin-dashboard.d.ts index bf19da0601..6d5b7ab3fd 100644 --- a/packages/dashboard/src/vaadin-dashboard.d.ts +++ b/packages/dashboard/src/vaadin-dashboard.d.ts @@ -11,14 +11,51 @@ import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js'; +export interface DashboardItem { + /** + * The column span of the item + */ + colspan?: number; +} + +export interface DashboardItemModel { + item: TItem; +} + +export type DashboardRenderer = ( + root: HTMLElement, + owner: Dashboard, + model: DashboardItemModel, +) => void; + /** * A responsive, grid-based dashboard layout component */ -declare class Dashboard extends DashboardLayoutMixin(ElementMixin(HTMLElement)) {} +declare class Dashboard extends DashboardLayoutMixin( + ElementMixin(HTMLElement), +) { + /** + * An array containing the items of the dashboard + */ + items: TItem[]; + + /** + * Custom function for rendering a widget for each dashboard item. + * Placing something else than a widget in the cell is not supported. + * Receives three arguments: + * + * - `root` The container for the widget. + * - `dashboard` The reference to the `` element. + * - `model` The object with the properties related with the rendered + * item, contains: + * - `model.item` The item. + */ + renderer: DashboardRenderer | null | undefined; +} declare global { interface HTMLElementTagNameMap { - 'vaadin-dashboard': Dashboard; + 'vaadin-dashboard': Dashboard; } } diff --git a/packages/dashboard/src/vaadin-dashboard.js b/packages/dashboard/src/vaadin-dashboard.js index 60bb5ad67f..2411b6cc48 100644 --- a/packages/dashboard/src/vaadin-dashboard.js +++ b/packages/dashboard/src/vaadin-dashboard.js @@ -9,10 +9,11 @@ * license. */ import './vaadin-dashboard-widget.js'; -import { html, LitElement } from 'lit'; +import { html, LitElement, render } from 'lit'; 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 { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js'; /** @@ -22,8 +23,9 @@ import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js'; * @extends HTMLElement * @mixes ElementMixin * @mixes DashboardLayoutMixin + * @mixes ThemableMixin */ -class Dashboard extends DashboardLayoutMixin(ElementMixin(PolylitMixin(LitElement))) { +class Dashboard extends DashboardLayoutMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) { static get is() { return 'vaadin-dashboard'; } @@ -32,9 +34,76 @@ class Dashboard extends DashboardLayoutMixin(ElementMixin(PolylitMixin(LitElemen return 'vaadin-dashboard'; } + static get styles() { + return [ + super.styles, + css` + ::slotted(vaadin-dashboard-cell) { + display: contents; + } + `, + ]; + } + + static get properties() { + return { + /** + * An array containing the items of the dashboard + * @type {!Array | null | undefined} + */ + items: { + type: Array, + }, + + /** + * Custom function for rendering a widget for each dashboard item. + * Placing something else than a widget in the cell is not supported. + * Receives three arguments: + * + * - `root` The container for the widget. + * - `dashboard` The reference to the `` element. + * - `model` The object with the properties related with the rendered + * item, contains: + * - `model.item` The item. + * + * @type {DashboardRenderer | null | undefined} + */ + renderer: { + type: Function, + }, + }; + } + + static get observers() { + return ['__itemsOrRendererChanged(items, renderer)']; + } + /** @protected */ render() { - return html``; + return html``; + } + + /** @private */ + __itemsOrRendererChanged(items, renderer) { + render(this.__renderItemCells(items || []), this); + + this.querySelectorAll('vaadin-dashboard-cell').forEach((cell) => { + if (renderer) { + renderer(cell, this, { item: cell.__item }); + } else { + cell.innerHTML = ''; + } + }); + } + + /** @private */ + __renderItemCells(items) { + return items.map((item) => { + return html``; + }); } } diff --git a/packages/dashboard/test/dashboard.test.ts b/packages/dashboard/test/dashboard.test.ts index 6b77f55281..373bef276f 100644 --- a/packages/dashboard/test/dashboard.test.ts +++ b/packages/dashboard/test/dashboard.test.ts @@ -1,14 +1,81 @@ import { expect } from '@vaadin/chai-plugins'; -import { fixtureSync } from '@vaadin/testing-helpers'; +import { fixtureSync, nextFrame } from '@vaadin/testing-helpers'; import '../vaadin-dashboard.js'; import type { CustomElementType } from '@vaadin/component-base/src/define.js'; -import type { Dashboard } from '../vaadin-dashboard.js'; +import type { Dashboard, DashboardItem } from '../vaadin-dashboard.js'; +import { getElementFromCell, setGap, setMaximumColumnWidth, setMinimumColumnWidth } from './helpers.js'; + +type TestDashboardItem = DashboardItem & { id: string }; describe('dashboard', () => { - let dashboard: Dashboard; + let dashboard: Dashboard; + const columnWidth = 100; - beforeEach(() => { + beforeEach(async () => { dashboard = fixtureSync(''); + dashboard.style.width = `${columnWidth * 100}px`; + setMinimumColumnWidth(dashboard, columnWidth); + setMaximumColumnWidth(dashboard, columnWidth); + setGap(dashboard, 0); + + dashboard.items = [{ id: 'Item 0' }, { id: 'Item 1' }]; + dashboard.renderer = (root, _, model) => { + root.textContent = ''; + const widget = document.createElement('vaadin-dashboard-widget'); + widget.widgetTitle = `${model.item.id} title`; + root.appendChild(widget); + }; + await nextFrame(); + }); + + it('should render a widget for each item', () => { + const widgets = [getElementFromCell(dashboard, 0, 0), getElementFromCell(dashboard, 0, 1)]; + widgets.forEach((widget, index) => { + expect(widget).to.be.ok; + expect(widget?.localName).to.equal('vaadin-dashboard-widget'); + expect(widget).to.have.property('widgetTitle', `Item ${index} title`); + }); + }); + + it('should render a new widget', async () => { + dashboard.items = [...dashboard.items, { id: 'Item 2' }]; + await nextFrame(); + + const newWidget = getElementFromCell(dashboard, 0, 2); + expect(newWidget).to.be.ok; + expect(newWidget?.localName).to.equal('vaadin-dashboard-widget'); + expect(newWidget).to.have.property('widgetTitle', 'Item 2 title'); + }); + + it('should update the renderer', async () => { + dashboard.renderer = (root, _, model) => { + root.textContent = ''; + const widget = document.createElement('vaadin-dashboard-widget'); + widget.widgetTitle = `${model.item.id} new title`; + root.appendChild(widget); + }; + await nextFrame(); + + const widgets = [getElementFromCell(dashboard, 0, 0), getElementFromCell(dashboard, 0, 1)]; + widgets.forEach((widget, index) => { + expect(widget).to.be.ok; + expect(widget?.localName).to.equal('vaadin-dashboard-widget'); + expect(widget).to.have.property('widgetTitle', `Item ${index} new title`); + }); + }); + + it('should clear the items', async () => { + dashboard.items = []; + await nextFrame(); + + expect(dashboard.querySelectorAll('vaadin-dashboard-widget')).to.be.empty; + }); + + it('should clear the renderer', async () => { + dashboard.renderer = undefined; + await nextFrame(); + + expect(dashboard.querySelectorAll('vaadin-dashboard-widget')).to.be.empty; }); describe('custom element definition', () => { @@ -26,4 +93,20 @@ describe('dashboard', () => { expect((customElements.get(tagName) as CustomElementType).is).to.equal(tagName); }); }); + + describe('column span', () => { + it('should span one column by default', () => { + const widgets = [getElementFromCell(dashboard, 0, 0), getElementFromCell(dashboard, 0, 1)]; + expect(widgets[0]).to.not.equal(widgets[1]); + }); + + it('should span multiple columns', async () => { + dashboard.items = [{ colspan: 2, id: 'Item 0' }]; + await nextFrame(); + + const widget = getElementFromCell(dashboard, 0, 0); + expect(widget).to.have.property('widgetTitle', 'Item 0 title'); + expect(getElementFromCell(dashboard, 0, 1)).to.equal(widget); + }); + }); }); diff --git a/packages/dashboard/test/helpers.ts b/packages/dashboard/test/helpers.ts index 911095be04..1b5551719a 100644 --- a/packages/dashboard/test/helpers.ts +++ b/packages/dashboard/test/helpers.ts @@ -27,7 +27,10 @@ export function getElementFromCell(dashboard: HTMLElement, rowIndex: number, col const x = left + columnWidths.slice(0, columnIndex).reduce((sum, width) => sum + width, 0); const y = top + rowHeights.slice(0, rowIndex).reduce((sum, height) => sum + height, 0); - return document.elementFromPoint(x + columnWidths[columnIndex] / 2, y + rowHeights[rowIndex] / 2); + return document + .elementsFromPoint(x + columnWidths[columnIndex] / 2, y + rowHeights[rowIndex] / 2) + .reverse() + .find((element) => dashboard.contains(element) && element !== dashboard)!; } /** diff --git a/packages/dashboard/test/typings/dashboard.types.ts b/packages/dashboard/test/typings/dashboard.types.ts index e7d1b9e94c..0781bdf9df 100644 --- a/packages/dashboard/test/typings/dashboard.types.ts +++ b/packages/dashboard/test/typings/dashboard.types.ts @@ -1,17 +1,28 @@ import type { ElementMixinClass } from '@vaadin/component-base/src/element-mixin.js'; import type { DashboardLayoutMixinClass } from '../../src/vaadin-dashboard-layout-mixin.js'; -import type { Dashboard } from '../../vaadin-dashboard.js'; +import type { Dashboard, DashboardItem, DashboardRenderer } from '../../vaadin-dashboard.js'; import type { DashboardLayout } from '../../vaadin-dashboard-layout.js'; import type { DashboardWidget } from '../../vaadin-dashboard-widget.js'; const assertType = (actual: TExpected) => actual; +interface TestDashboardItem extends DashboardItem { + testProperty: string; +} + /* Dashboard */ -const dashboard = document.createElement('vaadin-dashboard'); -assertType(dashboard); +const genericDashboard = document.createElement('vaadin-dashboard'); +assertType(genericDashboard); + +assertType(genericDashboard); +assertType(genericDashboard); +assertType(genericDashboard.items); -assertType(dashboard); -assertType(dashboard); +const narrowedDashboard = document.createElement('vaadin-dashboard') as unknown as Dashboard; +assertType>(narrowedDashboard); +assertType(narrowedDashboard.items); +assertType | null | undefined>(narrowedDashboard.renderer); +assertType<{ colspan?: number }>(narrowedDashboard.items[0]); /* DashboardLayout */ const layout = document.createElement('vaadin-dashboard-layout');