Skip to content

Commit 9243c36

Browse files
committed
feat: add root level heading support to dashboard layout
1 parent 0fb3dfc commit 9243c36

13 files changed

+293
-76
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
import { dashboardWidgetAndSectionStyles } from './vaadin-dashboard-styles.js';
1818

1919
const DEFAULT_I18N = {
@@ -58,6 +58,11 @@ export const DashboardItemMixin = (superClass) =>
5858
type: Object,
5959
},
6060

61+
/** @protected */
62+
_rootHeadingLevel: {
63+
type: Number,
64+
},
65+
6166
/** @private */
6267
__selected: {
6368
type: Boolean,
@@ -297,6 +302,19 @@ export const DashboardItemMixin = (superClass) =>
297302
this.addController(this.__focusTrapController);
298303
}
299304

305+
/** @protected */
306+
connectedCallback() {
307+
super.connectedCallback();
308+
this.__updateRootHeadingLevel();
309+
this.__setupHeadingLevelObserver();
310+
}
311+
312+
/** @protected */
313+
disconnectedCallback() {
314+
super.disconnectedCallback();
315+
this.__removeHeadingLevelObserver();
316+
}
317+
300318
/** @private */
301319
__selectedChanged(selected, oldSelected) {
302320
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
@@ -109,6 +109,21 @@ export const DashboardLayoutMixin = (superClass) =>
109109
value: false,
110110
reflectToAttribute: true,
111111
},
112+
113+
/**
114+
* Root heading level for sections and widgets. Defaults to 2.
115+
*
116+
* If changed to e.g. 1:
117+
* - sections will have the attribute `aria-level` with value 1
118+
* - non-nested widgets will have the attribute `aria-level` with value 1
119+
* - nested widgets will have the attribute `aria-level` with value 2
120+
*/
121+
rootHeadingLevel: {
122+
type: Number,
123+
value: 2,
124+
sync: true,
125+
reflectToAttribute: true,
126+
},
112127
};
113128
}
114129

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,6 @@ class DashboardSection extends DashboardItemMixin(ElementMixin(ThemableMixin(Pol
159159
value: '',
160160
},
161161

162-
/* @private */
163-
__rootHeadingLevel: {
164-
type: Number,
165-
},
166-
167162
/** @private */
168163
__childCount: {
169164
type: Number,
@@ -182,7 +177,7 @@ class DashboardSection extends DashboardItemMixin(ElementMixin(ThemableMixin(Pol
182177
183178
<header part="header">
184179
${this.__renderDragHandle()}
185-
<div id="title" role="heading" aria-level=${this.__rootHeadingLevel || 2} part="title"
180+
<div id="title" role="heading" aria-level=${this._rootHeadingLevel || 2} part="title"
186181
>${this.sectionTitle}</div
187182
>
188183
${this.__renderRemoveButton()}

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,6 @@ class DashboardWidget extends DashboardItemMixin(ElementMixin(ThemableMixin(Poly
203203
value: '',
204204
},
205205

206-
/* @private */
207-
__rootHeadingLevel: {
208-
type: Number,
209-
},
210-
211206
/* @private */
212207
__isNestedWidget: {
213208
type: Boolean,
@@ -249,7 +244,6 @@ class DashboardWidget extends DashboardItemMixin(ElementMixin(ThemableMixin(Poly
249244
this.toggleAttribute(attr, !!wrapper[attr]);
250245
});
251246
this.__i18n = wrapper.i18n;
252-
this.__rootHeadingLevel = wrapper.__rootHeadingLevel;
253247
}
254248

255249
this.__updateNestedState();
@@ -271,7 +265,7 @@ class DashboardWidget extends DashboardItemMixin(ElementMixin(ThemableMixin(Poly
271265

272266
/** @private */
273267
__renderWidgetTitle() {
274-
let effectiveHeadingLevel = this.__rootHeadingLevel;
268+
let effectiveHeadingLevel = this._rootHeadingLevel;
275269
// Default to 2 if not defined
276270
if (effectiveHeadingLevel == null) {
277271
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
@@ -166,20 +166,6 @@ class Dashboard extends DashboardLayoutMixin(
166166
type: Boolean,
167167
},
168168

169-
/**
170-
* Root heading level for sections and widgets. Defaults to 2.
171-
*
172-
* If changed to e.g. 1:
173-
* - sections will have the attribute `aria-level` with value 1
174-
* - non-nested widgets will have the attribute `aria-level` with value 1
175-
* - nested widgets will have the attribute `aria-level` with value 2
176-
*/
177-
rootHeadingLevel: {
178-
type: Number,
179-
value: 2,
180-
sync: true,
181-
},
182-
183169
/** @private */
184170
__childCount: {
185171
type: Number,
@@ -189,7 +175,7 @@ class Dashboard extends DashboardLayoutMixin(
189175
}
190176

191177
static get observers() {
192-
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n, rootHeadingLevel)'];
178+
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n)'];
193179
}
194180

195181
/**
@@ -275,7 +261,6 @@ class Dashboard extends DashboardLayoutMixin(
275261
wrapper.firstElementChild.toggleAttribute(attr, !!wrapper[attr]);
276262
});
277263
wrapper.firstElementChild.__i18n = this.__effectiveI18n;
278-
wrapper.firstElementChild.__rootHeadingLevel = this.rootHeadingLevel;
279264
}
280265
});
281266
}
@@ -319,7 +304,6 @@ class Dashboard extends DashboardLayoutMixin(
319304

320305
SYNCHRONIZED_ATTRIBUTES.forEach((attr) => section.toggleAttribute(attr, !!wrapper[attr]));
321306
section.__i18n = this.__effectiveI18n;
322-
section.__rootHeadingLevel = this.rootHeadingLevel;
323307

324308
// Render the subitems
325309
section.__childCount = item.items.length;
@@ -444,7 +428,6 @@ class Dashboard extends DashboardLayoutMixin(
444428
wrapper['first-child'] = item === getItemsArrayOfItem(item, this.items)[0];
445429
wrapper['last-child'] = item === getItemsArrayOfItem(item, this.items).slice(-1)[0];
446430
wrapper.i18n = this.__effectiveI18n;
447-
wrapper.__rootHeadingLevel = this.rootHeadingLevel;
448431
}
449432

450433
/** @private */

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

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

0 commit comments

Comments
 (0)