Skip to content

Commit 4794193

Browse files
committed
feat: add root level heading support to dashboard layout
1 parent 3f82f2c commit 4794193

13 files changed

+298
-81
lines changed

packages/dashboard/src/vaadin-dashboard-helpers.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,10 @@ export function fireRemove(element) {
102102
element.dispatchEvent(new CustomEvent('item-remove', { bubbles: true }));
103103
}
104104

105-
/**
106-
* Walks up the DOM tree starting from `node`, returning the first ancestor which is an instance of the given `baseClass`.
107-
*
108-
* @param {Node} node - starting node
109-
* @param {Function} baseClass - constructor, e.g. `Dashboard`
110-
* @returns {HTMLElement | null}
111-
*/
112-
export function findAncestorInstance(node, baseClass) {
105+
/** @private */
106+
function __findFilteredAncestorInstance(node, elementFilter) {
113107
while (node) {
114-
if (node instanceof baseClass) {
108+
if (elementFilter(node)) {
115109
return node;
116110
}
117111
if (node instanceof ShadowRoot) {
@@ -129,3 +123,28 @@ export function findAncestorInstance(node, baseClass) {
129123
}
130124
return null;
131125
}
126+
127+
/**
128+
* Walks up the DOM tree starting from `node`, returning the first ancestor which is an instance of the given `baseClass`.
129+
*
130+
* @param {Node} node - starting node
131+
* @param {Function} baseClass - constructor, e.g. `Dashboard`
132+
* @returns {HTMLElement | null}
133+
*/
134+
export function findAncestorInstance(node, baseClass) {
135+
return __findFilteredAncestorInstance(node, (el) => {
136+
return el instanceof baseClass;
137+
});
138+
}
139+
140+
/**
141+
* Walks up the DOM tree starting from `node`, returning the first ancestor which extends 'DashboardLayoutMixin'.
142+
*
143+
* @param {Node} node - starting node
144+
* @returns {HTMLElement | null}
145+
*/
146+
export function getParentLayout(node) {
147+
return __findFilteredAncestorInstance(node, (el) => {
148+
return el.constructor && el.constructor.toString().includes('DashboardLayoutMixin');
149+
});
150+
}

packages/dashboard/src/vaadin-dashboard-item-mixin.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { html } from 'lit';
1313
import { FocusTrapController } from '@vaadin/a11y-base/src/focus-trap-controller.js';
1414
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
1515
import { KeyboardController } from './keyboard-controller.js';
16-
import { fireMove, fireRemove, fireResize } from './vaadin-dashboard-helpers.js';
16+
import { fireMove, fireRemove, fireResize, getParentLayout } from './vaadin-dashboard-helpers.js';
1717

1818
const DEFAULT_I18N = {
1919
selectWidget: 'Select widget for editing',
@@ -53,6 +53,11 @@ export const DashboardItemMixin = (superClass) =>
5353
type: Object,
5454
},
5555

56+
/** @protected */
57+
_rootHeadingLevel: {
58+
type: Number,
59+
},
60+
5661
/** @private */
5762
__selected: {
5863
type: Boolean,
@@ -292,6 +297,19 @@ export const DashboardItemMixin = (superClass) =>
292297
this.addController(this.__focusTrapController);
293298
}
294299

300+
/** @protected */
301+
connectedCallback() {
302+
super.connectedCallback();
303+
this.__updateRootHeadingLevel();
304+
this.__setupHeadingLevelObserver();
305+
}
306+
307+
/** @protected */
308+
disconnectedCallback() {
309+
super.disconnectedCallback();
310+
this.__removeHeadingLevelObserver();
311+
}
312+
295313
/** @private */
296314
__selectedChanged(selected, oldSelected) {
297315
if (!!selected === !!oldSelected) {
@@ -377,4 +395,33 @@ export const DashboardItemMixin = (superClass) =>
377395
}
378396
this.dispatchEvent(new CustomEvent('item-resize-mode-changed', { bubbles: true, detail: { value: resizeMode } }));
379397
}
398+
399+
/** @private */
400+
__setupHeadingLevelObserver() {
401+
this.__removeHeadingLevelObserver();
402+
const parentLayout = getParentLayout(this);
403+
if (parentLayout) {
404+
this.__headingLevelObserver = new MutationObserver(() => this.__updateRootHeadingLevel());
405+
this.__headingLevelObserver.observe(parentLayout, {
406+
attributes: true,
407+
attributeFilter: ['root-heading-level'],
408+
});
409+
}
410+
}
411+
412+
/** @private */
413+
__removeHeadingLevelObserver() {
414+
if (this.__headingLevelObserver) {
415+
this.__headingLevelObserver.disconnect();
416+
this.__headingLevelObserver = null;
417+
}
418+
}
419+
420+
/** @private */
421+
__updateRootHeadingLevel() {
422+
const parentLayout = getParentLayout(this);
423+
if (parentLayout) {
424+
this._rootHeadingLevel = parentLayout.rootHeadingLevel;
425+
}
426+
}
380427
};

packages/dashboard/src/vaadin-dashboard-layout-mixin.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,14 @@ export declare class DashboardLayoutMixinClass {
2525
* @attr {boolean} dense-layout
2626
*/
2727
denseLayout: boolean;
28+
29+
/**
30+
* Root heading level for sections and widgets. Defaults to 2.
31+
*
32+
* If changed to e.g. 1:
33+
* - sections will have the attribute `aria-level` with value 1
34+
* - non-nested widgets will have the attribute `aria-level` with value 1
35+
* - nested widgets will have the attribute `aria-level` with value 2
36+
*/
37+
rootHeadingLevel: number | null | undefined;
2838
}

packages/dashboard/src/vaadin-dashboard-layout-mixin.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ export const DashboardLayoutMixin = (superClass) =>
3131
value: false,
3232
reflectToAttribute: true,
3333
},
34+
35+
/**
36+
* Root heading level for sections and widgets. Defaults to 2.
37+
*
38+
* If changed to e.g. 1:
39+
* - sections will have the attribute `aria-level` with value 1
40+
* - non-nested widgets will have the attribute `aria-level` with value 1
41+
* - nested widgets will have the attribute `aria-level` with value 2
42+
*/
43+
rootHeadingLevel: {
44+
type: Number,
45+
value: 2,
46+
sync: true,
47+
reflectToAttribute: true,
48+
},
3449
};
3550
}
3651

packages/dashboard/src/vaadin-dashboard-section.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,6 @@ class DashboardSection extends DashboardItemMixin(
126126
value: '',
127127
},
128128

129-
/* @private */
130-
__rootHeadingLevel: {
131-
type: Number,
132-
},
133-
134129
/** @private */
135130
__childCount: {
136131
type: Number,
@@ -149,7 +144,7 @@ class DashboardSection extends DashboardItemMixin(
149144
150145
<header part="header">
151146
${this.__renderDragHandle()}
152-
<div id="title" role="heading" aria-level=${this.__rootHeadingLevel || 2} part="title"
147+
<div id="title" role="heading" aria-level=${this._rootHeadingLevel || 2} part="title"
153148
>${this.sectionTitle}</div
154149
>
155150
${this.__renderRemoveButton()}

packages/dashboard/src/vaadin-dashboard-widget.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,6 @@ class DashboardWidget extends DashboardItemMixin(
166166
value: '',
167167
},
168168

169-
/* @private */
170-
__rootHeadingLevel: {
171-
type: Number,
172-
},
173-
174169
/* @private */
175170
__isNestedWidget: {
176171
type: Boolean,
@@ -212,7 +207,6 @@ class DashboardWidget extends DashboardItemMixin(
212207
this.toggleAttribute(attr, !!wrapper[attr]);
213208
});
214209
this.__i18n = wrapper.i18n;
215-
this.__rootHeadingLevel = wrapper.__rootHeadingLevel;
216210
}
217211

218212
this.__updateNestedState();
@@ -234,7 +228,7 @@ class DashboardWidget extends DashboardItemMixin(
234228

235229
/** @private */
236230
__renderWidgetTitle() {
237-
let effectiveHeadingLevel = this.__rootHeadingLevel;
231+
let effectiveHeadingLevel = this._rootHeadingLevel;
238232
// Default to 2 if not defined
239233
if (effectiveHeadingLevel == null) {
240234
effectiveHeadingLevel = 2;

packages/dashboard/src/vaadin-dashboard.d.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -243,16 +243,6 @@ declare class Dashboard<TItem extends DashboardItem = DashboardItem> extends Das
243243
*/
244244
editable: boolean;
245245

246-
/**
247-
* Root heading level for sections and widgets. Defaults to 2.
248-
*
249-
* If changed to e.g. 1:
250-
* - sections will have the attribute `aria-level` with value 1
251-
* - non-nested widgets will have the attribute `aria-level` with value 1
252-
* - nested widgets will have the attribute `aria-level` with value 2
253-
*/
254-
rootHeadingLevel: number | null | undefined;
255-
256246
/**
257247
* The object used to localize this component. To change the default
258248
* localization, replace this with an object that provides all properties, or

packages/dashboard/src/vaadin-dashboard.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -160,20 +160,6 @@ class Dashboard extends DashboardLayoutMixin(
160160
type: Boolean,
161161
},
162162

163-
/**
164-
* Root heading level for sections and widgets. Defaults to 2.
165-
*
166-
* If changed to e.g. 1:
167-
* - sections will have the attribute `aria-level` with value 1
168-
* - non-nested widgets will have the attribute `aria-level` with value 1
169-
* - nested widgets will have the attribute `aria-level` with value 2
170-
*/
171-
rootHeadingLevel: {
172-
type: Number,
173-
value: 2,
174-
sync: true,
175-
},
176-
177163
/** @private */
178164
__childCount: {
179165
type: Number,
@@ -183,7 +169,7 @@ class Dashboard extends DashboardLayoutMixin(
183169
}
184170

185171
static get observers() {
186-
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n, rootHeadingLevel)'];
172+
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n)'];
187173
}
188174

189175
/**
@@ -269,7 +255,6 @@ class Dashboard extends DashboardLayoutMixin(
269255
wrapper.firstElementChild.toggleAttribute(attr, !!wrapper[attr]);
270256
});
271257
wrapper.firstElementChild.__i18n = this.__effectiveI18n;
272-
wrapper.firstElementChild.__rootHeadingLevel = this.rootHeadingLevel;
273258
}
274259
});
275260
}
@@ -313,7 +298,6 @@ class Dashboard extends DashboardLayoutMixin(
313298

314299
SYNCHRONIZED_ATTRIBUTES.forEach((attr) => section.toggleAttribute(attr, !!wrapper[attr]));
315300
section.__i18n = this.__effectiveI18n;
316-
section.__rootHeadingLevel = this.rootHeadingLevel;
317301

318302
// Render the subitems
319303
section.__childCount = item.items.length;
@@ -438,7 +422,6 @@ class Dashboard extends DashboardLayoutMixin(
438422
wrapper['first-child'] = item === getItemsArrayOfItem(item, this.items)[0];
439423
wrapper['last-child'] = item === getItemsArrayOfItem(item, this.items).slice(-1)[0];
440424
wrapper.i18n = this.__effectiveI18n;
441-
wrapper.__rootHeadingLevel = this.rootHeadingLevel;
442425
}
443426

444427
/** @private */

packages/dashboard/test/dashboard-layout.test.ts

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { expect } from '@vaadin/chai-plugins';
22
import { fixtureSync, nextFrame, nextResize } from '@vaadin/testing-helpers';
3-
import '../src/vaadin-dashboard-layout.js';
4-
import '../src/vaadin-dashboard-section.js';
5-
import type { DashboardLayout } from '../src/vaadin-dashboard-layout.js';
6-
import type { DashboardSection } from '../src/vaadin-dashboard-section.js';
3+
import '../vaadin-dashboard-layout.js';
4+
import '../vaadin-dashboard-section.js';
5+
import '../vaadin-dashboard-widget.js';
6+
import type { DashboardLayout } from '../vaadin-dashboard-layout.js';
7+
import type { DashboardSection } from '../vaadin-dashboard-section.js';
8+
import type { DashboardWidget } from '../vaadin-dashboard-widget.js';
79
import {
10+
assertHeadingLevel,
811
expectLayout,
912
getColumnWidths,
1013
getRowHeights,
@@ -616,3 +619,73 @@ describe('dashboard layout', () => {
616619
});
617620
});
618621
});
622+
623+
describe('root heading level', () => {
624+
let dashboard: DashboardLayout;
625+
let section: DashboardSection;
626+
let widget: DashboardWidget;
627+
let nestedWidget: DashboardWidget;
628+
629+
beforeEach(async () => {
630+
dashboard = fixtureSync(`
631+
<vaadin-dashboard-layout>
632+
<vaadin-dashboard-widget widget-title="Widget"></vaadin-dashboard-widget>
633+
<vaadin-dashboard-section section-title="Section">
634+
<vaadin-dashboard-widget widget-title="Nested Widget"></vaadin-dashboard-widget>
635+
</vaadin-dashboard-section>
636+
</vaadin-dashboard-layout>
637+
`);
638+
await nextFrame();
639+
await nextResize(dashboard);
640+
widget = dashboard.querySelector('vaadin-dashboard-widget') as DashboardWidget;
641+
section = dashboard.querySelector('vaadin-dashboard-section') as DashboardSection;
642+
nestedWidget = section.querySelector('vaadin-dashboard-widget') as DashboardWidget;
643+
});
644+
645+
function assertHeadingLevels(expectedHeadingLevel: number) {
646+
assertHeadingLevel(widget, expectedHeadingLevel);
647+
assertHeadingLevel(section, expectedHeadingLevel);
648+
assertHeadingLevel(nestedWidget, expectedHeadingLevel + 1);
649+
}
650+
651+
it('should use default title heading level (2) when not explicitly set', () => {
652+
assertHeadingLevels(2);
653+
});
654+
655+
it('should use custom title heading level when set on dashboard layout', async () => {
656+
dashboard.rootHeadingLevel = 4;
657+
await nextFrame();
658+
assertHeadingLevels(4);
659+
});
660+
661+
it('should update title heading level when changed on dashboard layout', async () => {
662+
dashboard.rootHeadingLevel = 3;
663+
await nextFrame();
664+
dashboard.rootHeadingLevel = 1;
665+
await nextFrame();
666+
assertHeadingLevels(1);
667+
});
668+
669+
it('should revert to default title heading level (2) when set to null', async () => {
670+
dashboard.rootHeadingLevel = 4;
671+
await nextFrame();
672+
dashboard.rootHeadingLevel = null;
673+
await nextFrame();
674+
assertHeadingLevels(2);
675+
});
676+
677+
it('should update heading levels for newly added components', async () => {
678+
dashboard.rootHeadingLevel = 3;
679+
await nextFrame();
680+
const newWidget = document.createElement('vaadin-dashboard-widget');
681+
dashboard.appendChild(newWidget);
682+
const newSection = document.createElement('vaadin-dashboard-section');
683+
const nestedInNewSection = document.createElement('vaadin-dashboard-widget');
684+
newSection.appendChild(nestedInNewSection);
685+
dashboard.appendChild(newSection);
686+
await nextFrame();
687+
assertHeadingLevel(newWidget, 3);
688+
assertHeadingLevel(newSection, 3);
689+
assertHeadingLevel(nestedInNewSection, 4);
690+
});
691+
});

0 commit comments

Comments
 (0)