Skip to content

Commit

Permalink
feat: add basic dashboard section structure
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki committed Aug 22, 2024
1 parent f7051b0 commit 2be720f
Show file tree
Hide file tree
Showing 16 changed files with 422 additions and 39 deletions.
17 changes: 10 additions & 7 deletions dev/dashboard-layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<script type="module">
import '@vaadin/dashboard/vaadin-dashboard-layout.js';
import '@vaadin/dashboard/vaadin-dashboard-widget.js';
import '@vaadin/dashboard/vaadin-dashboard-section.js';
</script>

<style>
Expand Down Expand Up @@ -56,14 +57,16 @@
<div class="chart"></div>
</vaadin-dashboard-widget>

<vaadin-dashboard-widget widget-title="Sales closed this month">
<div class="kpi-number">54 000€</div>
</vaadin-dashboard-widget>
<vaadin-dashboard-section section-title="Section">
<vaadin-dashboard-widget widget-title="Sales closed this month">
<div class="kpi-number">54 000€</div>
</vaadin-dashboard-widget>

<vaadin-dashboard-widget widget-title="Just some number">
<span slot="header">2014-2024</span>
<div class="kpi-number">1234</div>
</vaadin-dashboard-widget>
<vaadin-dashboard-widget widget-title="Just some number">
<span slot="header">2014-2024</span>
<div class="kpi-number">1234</div>
</vaadin-dashboard-widget>
</vaadin-dashboard-section>

<vaadin-dashboard-widget>
<h2 slot="title">Activity since 2023</h2>
Expand Down
10 changes: 5 additions & 5 deletions packages/dashboard/src/title-controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
import { SlotChildObserveController } from '@vaadin/component-base/src/slot-child-observe-controller.js';

/**
* A controller to manage the widget title element.
* A controller to manage the widget or section title element.
*/
export class TitleController extends SlotChildObserveController {
/**
* String used for the widget title.
* String used for the title.
*/
protected widgetTitle: string | null | undefined;
protected title: string | null | undefined;

/**
* Set widget title based on corresponding host property.
* Set title based on corresponding host property.
*/
setWidgetTitle(widgetTitle: string | null | undefined): void;
setTitle(title: string | null | undefined): void;
}
24 changes: 12 additions & 12 deletions packages/dashboard/src/title-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,36 @@
import { SlotChildObserveController } from '@vaadin/component-base/src/slot-child-observe-controller.js';

/**
* A controller to manage the widget title element.
* A controller to manage the widget or section title element.
*/
export class TitleController extends SlotChildObserveController {
constructor(host) {
super(host, 'title', null);
}

/**
* Set widget title based on corresponding host property.
* Set title based on corresponding host property.
*
* @param {string} widgetTitle
* @param {string} title
*/
setWidgetTitle(widgetTitle) {
this.widgetTitle = widgetTitle;
setTitle(title) {
this.title = title;

// Restore the default widgetTitle, if needed.
const widgetTitleNode = this.getSlotChild();
if (!widgetTitleNode) {
// Restore the default title, if needed.
const titleNode = this.getSlotChild();
if (!titleNode) {
this.restoreDefaultNode();
}

// When default widgetTitle is used, update it.
// When default title is used, update it.
if (this.node === this.defaultNode) {
this.updateDefaultNode(this.node);
}
}

/**
* Override method inherited from `SlotChildObserveController`
* to restore and observe the default widget title element.
* to restore and observe the default title element.
*
* @protected
* @override
Expand All @@ -47,15 +47,15 @@ export class TitleController extends SlotChildObserveController {

/**
* Override method inherited from `SlotChildObserveController`
* to update the default widgetTitle element text content.
* to update the default title element text content.
*
* @param {Node | undefined} node
* @protected
* @override
*/
updateDefaultNode(node) {
if (node) {
node.textContent = this.widgetTitle;
node.textContent = this.title;
}

// Notify the host after update.
Expand Down
30 changes: 30 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-section.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @license
* Copyright (c) 2000 - 2024 Vaadin Ltd.
*
* This program is available under Vaadin Commercial License and Service Terms.
*
*
* See https://vaadin.com/commercial-license-and-service-terms for the full
* license.
*/
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';

/**
* A Section component for use with the Dashboard component
*/
declare class DashboardSection extends ControllerMixin(ElementMixin(HTMLElement)) {
/**
* The title of the section
*/
sectionTitle: string | null | undefined;
}

declare global {
interface HTMLElementTagNameMap {
'vaadin-dashboard-section': DashboardSection;
}
}

export { DashboardSection };
109 changes: 109 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-section.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @license
* Copyright (c) 2000 - 2024 Vaadin Ltd.
*
* This program is available under Vaadin Commercial License and Service Terms.
*
*
* See https://vaadin.com/commercial-license-and-service-terms for the full
* license.
*/
import { html, LitElement } from 'lit';
import { ControllerMixin } from '@vaadin/component-base/src/controller-mixin.js';
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 } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { TitleController } from './title-controller.js';

/**
* A section component for use with the Dashboard component
*
* @customElement
* @extends HTMLElement
* @mixes ElementMixin
* @mixes ControllerMixin
*/
class DashboardSection extends ControllerMixin(ElementMixin(PolylitMixin(LitElement))) {
static get is() {
return 'vaadin-dashboard-section';
}

static get styles() {
return css`
:host {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / -1 !important;
gap: var(--vaadin-dashboard-gap, 1rem);
}
:host([hidden]) {
display: none !important;
}
header {
display: flex;
grid-column: 1 / -1;
justify-content: space-between;
align-items: center;
}
`;
}

static get properties() {
return {
/**
* The title of the section
*/
sectionTitle: {
type: String,
value: '',
observer: '__onsectionTitleChanged',
},
};
}

/** @protected */
render() {
return html`
<header>
<slot name="title" @slotchange="${this.__onTitleSlotChange}"></slot>
<slot name="header"></slot>
<div id="header-actions"></div>
</header>
<slot></slot>
`;
}

constructor() {
super();
this.__titleController = new TitleController(this);
this.__titleController.addEventListener('slot-content-changed', (event) => {
const { node } = event.target;
if (node) {
this.setAttribute('aria-labelledby', node.id);
}
});
}

/** @protected */
ready() {
super.ready();
this.addController(this.__titleController);

if (!this.hasAttribute('role')) {
this.setAttribute('role', 'section');
}
}

/** @private */
__onsectionTitleChanged(sectionTitle) {
this.__titleController.setTitle(sectionTitle);
}
}

defineCustomElement(DashboardSection);

export { DashboardSection };
2 changes: 1 addition & 1 deletion packages/dashboard/src/vaadin-dashboard-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme

/** @private */
__onWidgetTitleChanged(widgetTitle) {
this.__titleController.setWidgetTitle(widgetTitle);
this.__titleController.setTitle(widgetTitle);
}
}

Expand Down
102 changes: 100 additions & 2 deletions packages/dashboard/test/dashboard-layout.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from '@vaadin/chai-plugins';
import { fixtureSync, nextFrame } from '@vaadin/testing-helpers';
import '../vaadin-dashboard-layout.js';
import '../vaadin-dashboard-section.js';
import type { DashboardLayout } from '../vaadin-dashboard-layout.js';
import {
getColumnWidths,
Expand Down Expand Up @@ -34,15 +35,15 @@ import {
* ]
* ```
*/
function expectLayout(dashboard: DashboardLayout, layout: number[][]) {
function expectLayout(dashboard: DashboardLayout, layout: Array<Array<number | null>>) {
expect(getRowHeights(dashboard).length).to.eql(layout.length);
expect(getColumnWidths(dashboard).length).to.eql(layout[0].length);

layout.forEach((row, rowIndex) => {
row.forEach((itemId, columnIndex) => {
const element = getElementFromCell(dashboard, rowIndex, columnIndex);
if (!element) {
expect(itemId).to.be.undefined;
expect(itemId).to.be.null;
} else {
expect(element.id).to.equal(`item-${itemId}`);
}
Expand Down Expand Up @@ -311,4 +312,101 @@ describe('dashboard layout', () => {
expect(getColumnWidths(dashboard).length).to.eql(20);
});
});

describe('section', () => {
beforeEach(async () => {
const section = fixtureSync(`
<vaadin-dashboard-section>
<div id="item-2">Section item 2</div>
<div id="item-3">Section item 3</div>
</vaadin-dashboard-section>
`);
dashboard.appendChild(section);
await nextFrame();
childElements = [...dashboard.querySelectorAll('div')];
});

it('should span full width of the dashboard layout', () => {
/* prettier-ignore */
expectLayout(dashboard, [
[0, 1],
[2, 3],
]);
});

it('should be on its own row', async () => {
dashboard.style.width = `${columnWidth * 4}px`;
await nextFrame();

/* prettier-ignore */
expectLayout(dashboard, [
[0, 1, null, null],
[2, 3, null, null],
]);
});

it('following items should end up in the next row', async () => {
dashboard.style.width = `${columnWidth * 4}px`;
dashboard.appendChild(fixtureSync('<div id="item-4">Item 4</div>'));
await nextFrame();

/* prettier-ignore */
expectLayout(dashboard, [
[0, 1, null, null],
[2, 3, null, null],
[4],
]);
});

it('should be capped to currently available columns', async () => {
dashboard.style.width = `${columnWidth}px`;
await nextFrame();

/* prettier-ignore */
expectLayout(dashboard, [
[0],
[1],
[2],
[3],
]);
});

describe('gap', () => {
it('should have a default gap', () => {
// Clear the gap used in the tests
setGap(dashboard, undefined);
// Increase the width of the dashboard to fit two items and a gap
dashboard.style.width = `${columnWidth * 2 + remValue}px`;

const { right: item2Right } = childElements[2].getBoundingClientRect();
const { left: item3Left } = childElements[3].getBoundingClientRect();
// Expect the items to have a gap of 1rem
expect(item3Left - item2Right).to.eql(remValue);
});

it('should have a custom gap between items horizontally', () => {
const customGap = 10;
setGap(dashboard, customGap);
// Increase the width of the dashboard to fit two items and a gap
dashboard.style.width = `${columnWidth * 2 + customGap}px`;

const { right: item2Right } = childElements[2].getBoundingClientRect();
const { left: item3Left } = childElements[3].getBoundingClientRect();
// Expect the items to have a gap of 10px
expect(item3Left - item2Right).to.eql(customGap);
});

it('should have a custom gap between items vertically', async () => {
const customGap = 10;
setGap(dashboard, customGap);
dashboard.style.width = `${columnWidth}px`;
await nextFrame();

const { bottom: item2Bottom } = childElements[2].getBoundingClientRect();
const { top: item3Top } = childElements[3].getBoundingClientRect();
// Expect the items to have a gap of 10px
expect(item3Top - item2Bottom).to.eql(customGap);
});
});
});
});
Loading

0 comments on commit 2be720f

Please sign in to comment.