Skip to content

Commit

Permalink
feat: dashboard keyboard interactions
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki committed Sep 17, 2024
1 parent 9426214 commit 04cfc9c
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 30 deletions.
13 changes: 7 additions & 6 deletions dev/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
<style>
vaadin-dashboard-widget {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 10px;
}
Expand Down Expand Up @@ -82,11 +81,13 @@
];

dashboard.renderer = (root, _dashboard, { item }) => {
root.innerHTML = `
<vaadin-dashboard-widget widget-title="${item.title}">
<span slot="header">${item.header || ''}</span>
${item.type === 'chart' ? '<div class="chart"></div>' : `<div class="kpi-number">${item.content}</div>`}
</vaadin-dashboard-widget>
if (!root.firstElementChild) {
root.append(document.createElement('vaadin-dashboard-widget'));
}
root.firstElementChild.widgetTitle = item.title;
root.firstElementChild.innerHTML = `
<span slot="header">${item.header || ''}</span>
${item.type === 'chart' ? '<div class="chart"></div>' : `<div class="kpi-number">${item.content}</div>`}
`;
};

Expand Down
71 changes: 71 additions & 0 deletions packages/dashboard/src/keyboard-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @license
* Copyright (c) 2019 - 2024 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/

import { fireMove, fireRemove, fireResize } from './vaadin-dashboard-helpers.js';

/**
* A controller for managing widget/section keyboard interactions.
*/
export class KeyboardController {
constructor(host) {
this.host = host;

host.addEventListener('focusout', (e) => this.__focusout(e));
host.addEventListener('focusin', (e) => this.__focusin(e));
host.addEventListener('keydown', (e) => this.__keydown(e));
}

/** @private */
__focusout() {
this.host.__focused = false;
this.host.__selected = false;
}

/** @private */
__focusin(e) {
if (e.target === this.host) {
this.host.__focused = true;
}
}

/** @private */
__keydown(e) {
if (e.metaKey || e.ctrlKey || !this.host.__selected) {
return;
}

if (e.key === 'Backspace' || e.key === 'Delete') {
e.preventDefault();
fireRemove(this.host);
} else if (e.key === 'Escape') {
e.preventDefault();
this.host.__selected = false;
this.host.focus();
} else if (e.shiftKey) {
const resizeMap = {
ArrowRight: [document.dir === 'rtl' ? -1 : 1, 0],
ArrowLeft: [document.dir === 'rtl' ? 1 : -1, 0],
ArrowDown: [0, 1],
ArrowUp: [0, -1],
};
if (resizeMap[e.key]) {
e.preventDefault();
fireResize(this.host, ...resizeMap[e.key]);
}
} else {
const moveMap = {
ArrowRight: document.dir === 'rtl' ? -1 : 1,
ArrowLeft: document.dir === 'rtl' ? 1 : -1,
ArrowDown: 1,
ArrowUp: -1,
};
if (moveMap[e.key]) {
e.preventDefault();
fireMove(this.host, moveMap[e.key]);
}
}
}
}
45 changes: 45 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,48 @@ export function getItemsArrayOfItem(item, items) {
export function getElementItem(element) {
return element.closest(WRAPPER_LOCAL_NAME).__item;
}

/**
* Dispatches a custom event to notify about a move operation.
*
* @param {HTMLElement} element
* @param {Number} delta
*/
export function fireMove(element, delta) {
element.dispatchEvent(
new CustomEvent('item-move', {
bubbles: true,
composed: true,
detail: { delta },
}),
);
}

/**
* Dispatches a custom event to notify about a resize operation.
*
* @param {HTMLElement} element
* @param {Number} colspanDelta
* @param {Number} rowspanDelta
*/
export function fireResize(element, colspanDelta, rowspanDelta) {
element.dispatchEvent(
new CustomEvent('item-resize', {
bubbles: true,
composed: true,
detail: {
colspanDelta,
rowspanDelta,
},
}),
);
}

/**
* Dispatches a custom event to notify about a remove operation.
*
* @param {HTMLElement} element
*/
export function fireRemove(element) {
element.dispatchEvent(new CustomEvent('item-remove', { bubbles: true, composed: true }));
}
41 changes: 36 additions & 5 deletions packages/dashboard/src/vaadin-dashboard-section.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ 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 { KeyboardController } from './keyboard-controller.js';
import { TitleController } from './title-controller.js';
import { fireRemove } from './vaadin-dashboard-helpers.js';
import { dashboardWidgetAndSectionStyles, hasWidgetWrappers } from './vaadin-dashboard-styles.js';

/**
Expand Down Expand Up @@ -93,16 +95,40 @@ class DashboardSection extends ControllerMixin(ElementMixin(PolylitMixin(LitElem
value: '',
observer: '__onSectionTitleChanged',
},

/** @private */
__selected: {
type: Boolean,
reflectToAttribute: true,
attribute: 'selected',
},

/** @private */
__focused: {
type: Boolean,
reflectToAttribute: true,
attribute: 'focused',
},
};
}

/** @protected */
render() {
return html`
<button
aria-label="Select Section Title for editing"
id="focus-button"
draggable="true"
class="drag-handle"
@click="${() => {
this.__selected = true;
}}"
></button>
<header>
<button id="drag-handle" draggable="true" class="drag-handle" tabindex="-1"></button>
<button id="drag-handle" draggable="true" class="drag-handle" tabindex="${this.__selected ? 0 : -1}"></button>
<slot name="title" @slotchange="${this.__onTitleSlotChange}"></slot>
<button id="remove-button" tabindex="-1" @click="${() => this.__remove()}"></button>
<button id="remove-button" tabindex="${this.__selected ? 0 : -1}" @click="${() => fireRemove(this)}"></button>
</header>
<slot></slot>
Expand All @@ -111,6 +137,7 @@ class DashboardSection extends ControllerMixin(ElementMixin(PolylitMixin(LitElem

constructor() {
super();
this.__keyboardController = new KeyboardController(this);
this.__titleController = new TitleController(this);
this.__titleController.addEventListener('slot-content-changed', (event) => {
const { node } = event.target;
Expand All @@ -123,6 +150,7 @@ class DashboardSection extends ControllerMixin(ElementMixin(PolylitMixin(LitElem
/** @protected */
ready() {
super.ready();
this.addController(this.__keyboardController);
this.addController(this.__titleController);

if (!this.hasAttribute('role')) {
Expand All @@ -135,9 +163,12 @@ class DashboardSection extends ControllerMixin(ElementMixin(PolylitMixin(LitElem
this.__titleController.setTitle(sectionTitle);
}

/** @private */
__remove() {
this.dispatchEvent(new CustomEvent('item-remove', { bubbles: true, composed: true }));
focus() {
if (this.hasAttribute('editable')) {
this.$['focus-button'].focus();
} else {
super.focus();
}
}
}

Expand Down
21 changes: 19 additions & 2 deletions packages/dashboard/src/vaadin-dashboard-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,25 @@ export const dashboardWidgetAndSectionStyles = css`
box-sizing: border-box;
}
:host([focused]) {
border: 1px solid blue;
}
:host([selected]) {
border: 4px solid red;
}
:host([dragging]) {
border: 3px dashed black !important;
border: 3px dashed black;
}
:host([dragging]) * {
visibility: hidden;
}
:host(:not([editable])) #drag-handle,
:host(:not([editable])) #remove-button {
:host(:not([editable])) #remove-button,
:host(:not([editable])) #focus-button {
display: none;
}
Expand All @@ -30,9 +39,16 @@ export const dashboardWidgetAndSectionStyles = css`
align-items: center;
}
#focus-button {
position: absolute;
inset: 0;
opacity: 0;
}
#drag-handle {
font-size: 30px;
cursor: grab;
z-index: 1;
}
#drag-handle::before {
Expand All @@ -42,6 +58,7 @@ export const dashboardWidgetAndSectionStyles = css`
#remove-button {
font-size: 30px;
cursor: pointer;
z-index: 1;
}
#remove-button::before {
Expand Down
45 changes: 38 additions & 7 deletions packages/dashboard/src/vaadin-dashboard-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ 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 { KeyboardController } from './keyboard-controller.js';
import { TitleController } from './title-controller.js';
import { SYNCHRONIZED_ATTRIBUTES, WRAPPER_LOCAL_NAME } from './vaadin-dashboard-helpers.js';
import { fireRemove, SYNCHRONIZED_ATTRIBUTES, WRAPPER_LOCAL_NAME } from './vaadin-dashboard-helpers.js';
import { dashboardWidgetAndSectionStyles } from './vaadin-dashboard-styles.js';

/**
Expand Down Expand Up @@ -62,6 +63,7 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
font-size: 30px;
cursor: grab;
line-height: 1;
z-index: 1;
}
#resize-handle::before {
Expand Down Expand Up @@ -97,29 +99,54 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
value: '',
observer: '__onWidgetTitleChanged',
},

/** @private */
__selected: {
type: Boolean,
reflectToAttribute: true,
attribute: 'selected',
},

/** @private */
__focused: {
type: Boolean,
reflectToAttribute: true,
attribute: 'focused',
},
};
}

/** @protected */
render() {
return html`
<button
aria-label="Select Widget Title for editing"
id="focus-button"
draggable="true"
class="drag-handle"
@click="${() => {
this.__selected = true;
}}"
></button>
<header>
<button id="drag-handle" draggable="true" class="drag-handle" tabindex="-1"></button>
<button id="drag-handle" draggable="true" class="drag-handle" tabindex="${this.__selected ? 0 : -1}"></button>
<slot name="title" @slotchange="${this.__onTitleSlotChange}"></slot>
<slot name="header"></slot>
<button id="remove-button" tabindex="-1" @click="${() => this.__remove()}"></button>
<button id="remove-button" tabindex="${this.__selected ? 0 : -1}" @click="${() => fireRemove(this)}"></button>
</header>
<div id="content">
<slot></slot>
</div>
<button id="resize-handle" class="resize-handle" tabindex="-1"></button>
<button id="resize-handle" class="resize-handle" tabindex="${this.__selected ? 0 : -1}"></button>
`;
}

constructor() {
super();
this.__keyboardController = new KeyboardController(this);
this.__titleController = new TitleController(this);
this.__titleController.addEventListener('slot-content-changed', (event) => {
const { node } = event.target;
Expand Down Expand Up @@ -152,6 +179,7 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
ready() {
super.ready();
this.addController(this.__titleController);
this.addController(this.__keyboardController);

if (!this.hasAttribute('role')) {
this.setAttribute('role', 'article');
Expand All @@ -168,9 +196,12 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
this.__titleController.setTitle(this.widgetTitle);
}

/** @private */
__remove() {
this.dispatchEvent(new CustomEvent('item-remove', { bubbles: true, composed: true }));
focus() {
if (this.hasAttribute('editable')) {
this.$['focus-button'].focus();
} else {
super.focus();
}
}
}

Expand Down
Loading

0 comments on commit 04cfc9c

Please sign in to comment.