Skip to content

Commit

Permalink
feat: add vaadin-dashboard-layout (#7653)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki authored Aug 16, 2024
1 parent 2134205 commit 8864042
Show file tree
Hide file tree
Showing 14 changed files with 409 additions and 2 deletions.
42 changes: 42 additions & 0 deletions dev/dashboard-layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard layout</title>
<script type="module" src="./common.js"></script>

<script type="module">
import '@vaadin/dashboard/vaadin-dashboard-layout.js';
</script>

<style>
vaadin-dashboard-layout div {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 1em;
text-align: center;
margin: 0.5em;
box-sizing: border-box;
height: 100px;
}

vaadin-dashboard-layout {
--vaadin-dashboard-col-min-width: 300px;
--vaadin-dashboard-col-max-width: 500px;
}
</style>
</head>

<body>
<vaadin-dashboard-layout>
<div>Item 0</div>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
<div>Item 4</div>
</vaadin-dashboard-layout>
</body>
</html>
20 changes: 20 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-layout-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @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 type { Constructor } from '@open-wc/dedupe-mixin';

/**
* A mixin to enable the dashboard layout functionality.
*/
export declare function DashboardLayoutMixin<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<DashboardLayoutMixinClass> & T;

export declare class DashboardLayoutMixinClass {}
44 changes: 44 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-layout-mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @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 { css } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';

/**
* A mixin to enable the dashboard layout functionality
*
* @polymerMixin
*/
export const DashboardLayoutMixin = (superClass) =>
class DashboardLayoutMixinClass extends superClass {
static get styles() {
return css`
:host {
display: grid;
/* Default min and max column widths */
--_vaadin-dashboard-default-col-min-width: 25rem;
--_vaadin-dashboard-default-col-max-width: 1fr;
/* Effective min and max column widths */
--_vaadin-dashboard-col-min-width: var(
--vaadin-dashboard-col-min-width,
var(--_vaadin-dashboard-default-col-min-width)
);
--_vaadin-dashboard-col-max-width: var(
--vaadin-dashboard-col-max-width,
var(--_vaadin-dashboard-default-col-max-width)
);
grid-template-columns: repeat(
auto-fill,
minmax(var(--_vaadin-dashboard-col-min-width), var(--_vaadin-dashboard-col-max-width))
);
}
`;
}
};
25 changes: 25 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-layout.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @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 { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';

/**
* A responsive, grid-based dashboard layout component
*/
declare class DashboardLayout extends ElementMixin(ThemableMixin(HTMLElement)) {}

declare global {
interface HTMLElementTagNameMap {
'vaadin-dashboard-layout': DashboardLayout;
}
}

export { DashboardLayout };
40 changes: 40 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @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 { 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 { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js';

/**
* A responsive, grid-based dashboard layout component
*
* @customElement
* @extends HTMLElement
* @mixes DashboardLayoutMixin
* @mixes ElementMixin
* @mixes ThemableMixin
*/
class DashboardLayout extends DashboardLayoutMixin(ElementMixin(ThemableMixin(PolylitMixin(LitElement)))) {
static get is() {
return 'vaadin-dashboard-layout';
}

/** @protected */
render() {
return html`<slot></slot>`;
}
}

defineCustomElement(DashboardLayout);

export { DashboardLayout };
3 changes: 2 additions & 1 deletion packages/dashboard/src/vaadin-dashboard.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
* license.
*/
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js';

/**
* A responsive, grid-based dashboard layout component
*/
declare class Dashboard extends ElementMixin(HTMLElement) {}
declare class Dashboard extends DashboardLayoutMixin(ElementMixin(HTMLElement)) {}

declare global {
interface HTMLElementTagNameMap {
Expand Down
4 changes: 3 additions & 1 deletion packages/dashboard/src/vaadin-dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import { html, LitElement } 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 { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js';

/**
* A responsive, grid-based dashboard layout component
*
* @customElement
* @extends HTMLElement
* @mixes ElementMixin
* @mixes DashboardLayoutMixin
*/
class Dashboard extends ElementMixin(PolylitMixin(LitElement)) {
class Dashboard extends DashboardLayoutMixin(ElementMixin(PolylitMixin(LitElement))) {
static get is() {
return 'vaadin-dashboard';
}
Expand Down
155 changes: 155 additions & 0 deletions packages/dashboard/test/dashboard-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { expect } from '@vaadin/chai-plugins';
import { fixtureSync, nextFrame } from '@vaadin/testing-helpers';
import '../vaadin-dashboard-layout.js';
import type { DashboardLayout } from '../vaadin-dashboard-layout.js';
import {
getColumnWidths,
getElementFromCell,
setGap,
setMaximumColumnWidth,
setMinimumColumnWidth,
} from './helpers.js';

/**
* Validates the given grid layout.
*
* This function iterates through a number matrix representing the IDs of
* the items in the layout, and checks if the elements in the corresponding
* cells of the grid match the expected IDs.
*
* For example, the following layout would expect a grid with two columns
* and three rows, where the first row has one element with ID "item-0" spanning
* two columns, and the second row has two elements with IDs "item-1" and "item-2"
* where the first one spans two rows, and the last cell in the third row has
* an element with ID "item-3":
*
* ```
* [
* [0, 0],
* [1, 2],
* [1, 3]
* ]
* ```
*/
function expectLayout(dashboard: DashboardLayout, layout: number[][]) {
layout.forEach((row, rowIndex) => {
row.forEach((itemId, columnIndex) => {
const element = getElementFromCell(dashboard, rowIndex, columnIndex);
if (!element) {
expect(itemId).to.be.undefined;
} else {
expect(element.id).to.equal(`item-${itemId}`);
}
});
});
}

describe('dashboard layout', () => {
let dashboard: DashboardLayout;
const columnWidth = 100;
const remValue = parseFloat(getComputedStyle(document.documentElement).fontSize);

beforeEach(async () => {
dashboard = fixtureSync(`
<vaadin-dashboard-layout>
<div id="item-0">Item 0</div>
<div id="item-1">Item 1</div>
</vaadin-dashboard-layout>
`);
// Disable gap between items in these tests
setGap(dashboard, 0);
// Set the column width to a fixed value
setMinimumColumnWidth(dashboard, columnWidth);
setMaximumColumnWidth(dashboard, columnWidth);
// Make the dashboard wide enough to fit all items on a single row
dashboard.style.width = `${columnWidth * dashboard.childElementCount}px`;

await nextFrame();

expect(getColumnWidths(dashboard)).to.eql([columnWidth, columnWidth]);
/* prettier-ignore */
expectLayout(dashboard, [
[0, 1],
]);
});

it('should be responsive', () => {
// Narrow down the component to fit one column
dashboard.style.width = `${columnWidth}px`;

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

describe('minimum column width', () => {
it('should have a default minimum column width', () => {
// Clear the minimum column width used in the tests
setMinimumColumnWidth(dashboard, undefined);
// Narrow down the component to have the width of 0
dashboard.style.width = '0';
// Expect the column width to equal the default minimum column width
expect(getColumnWidths(dashboard)).to.eql([remValue * 25]);
});

it('should have one overflown column if narrowed below minimum column width', () => {
// Narrow down the component to have the width of half a column
dashboard.style.width = `${columnWidth / 2}px`;
// Expect the column width to still be the same (overflown)
expect(getColumnWidths(dashboard)).to.eql([columnWidth]);
});

it('should not overflow if narrowed to the minimum column width', () => {
// Set the min column width to half of the column width
setMinimumColumnWidth(dashboard, columnWidth / 2);
// Narrow down the component to have the width of half a column
dashboard.style.width = `${columnWidth / 2}px`;
// Expect the column width to equal the min column width
expect(getColumnWidths(dashboard)).to.eql([columnWidth / 2]);
});

it('should have one wide column with large minimum column width', () => {
setMaximumColumnWidth(dashboard, columnWidth * 2);
// Set the min column width to be twice as wide
setMinimumColumnWidth(dashboard, columnWidth * 2);
// Expect there to only be one column with twice the width
expect(getColumnWidths(dashboard)).to.eql([columnWidth * 2]);
/* prettier-ignore */
expectLayout(dashboard, [
[0],
[1],
]);
});
});

describe('maximum column width', () => {
it('should have a default maximum column width', () => {
// Clear the maximum column width used in the tests
setMaximumColumnWidth(dashboard, undefined);
expect(getColumnWidths(dashboard)).to.eql([columnWidth, columnWidth]);
// Widen the component to have the width of 2.5 columns
dashboard.style.width = `${columnWidth * 2.5}px`;
expect(getColumnWidths(dashboard)).to.eql([columnWidth * 1.25, columnWidth * 1.25]);
// Widen the component to have the width of 3 columns
dashboard.style.width = `${columnWidth * 3}px`;
expect(getColumnWidths(dashboard)).to.eql([columnWidth, columnWidth, columnWidth]);
// Shrink the component to have the width of 1.5 columns
dashboard.style.width = `${columnWidth * 1.5}px`;
expect(getColumnWidths(dashboard)).to.eql([columnWidth * 1.5]);
});

it('should have one wide column with large maximum column width', () => {
// Allow the column to be twice as wide
setMaximumColumnWidth(dashboard, columnWidth * 2);
// Expect there to only be one column with twice the width
expect(getColumnWidths(dashboard)).to.eql([columnWidth * 2]);
/* prettier-ignore */
expectLayout(dashboard, [
[0],
[1],
]);
});
});
});
Loading

0 comments on commit 8864042

Please sign in to comment.