Skip to content

Commit 9bc1ebc

Browse files
committed
feat: add root level heading support to dashboard layout
1 parent eac786b commit 9bc1ebc

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
@@ -155,11 +155,6 @@ class DashboardSection extends DashboardItemMixin(ElementMixin(ThemableMixin(Pol
155155
value: '',
156156
},
157157

158-
/* @private */
159-
__rootHeadingLevel: {
160-
type: Number,
161-
},
162-
163158
/** @private */
164159
__childCount: {
165160
type: Number,
@@ -178,7 +173,7 @@ class DashboardSection extends DashboardItemMixin(ElementMixin(ThemableMixin(Pol
178173
179174
<header part="header">
180175
${this.__renderDragHandle()}
181-
<div id="title" role="heading" aria-level=${this.__rootHeadingLevel || 2} part="title"
176+
<div id="title" role="heading" aria-level=${this._rootHeadingLevel || 2} part="title"
182177
>${this.sectionTitle}</div
183178
>
184179
${this.__renderRemoveButton()}

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,6 @@ class DashboardWidget extends DashboardItemMixin(ElementMixin(ThemableMixin(Poly
199199
value: '',
200200
},
201201

202-
/* @private */
203-
__rootHeadingLevel: {
204-
type: Number,
205-
},
206-
207202
/* @private */
208203
__isNestedWidget: {
209204
type: Boolean,
@@ -245,7 +240,6 @@ class DashboardWidget extends DashboardItemMixin(ElementMixin(ThemableMixin(Poly
245240
this.toggleAttribute(attr, !!wrapper[attr]);
246241
});
247242
this.__i18n = wrapper.i18n;
248-
this.__rootHeadingLevel = wrapper.__rootHeadingLevel;
249243
}
250244

251245
this.__updateNestedState();
@@ -267,7 +261,7 @@ class DashboardWidget extends DashboardItemMixin(ElementMixin(ThemableMixin(Poly
267261

268262
/** @private */
269263
__renderWidgetTitle() {
270-
let effectiveHeadingLevel = this.__rootHeadingLevel;
264+
let effectiveHeadingLevel = this._rootHeadingLevel;
271265
// Default to 2 if not defined
272266
if (effectiveHeadingLevel == null) {
273267
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
@@ -162,20 +162,6 @@ class Dashboard extends DashboardLayoutMixin(
162162
type: Boolean,
163163
},
164164

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

187173
static get observers() {
188-
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n, rootHeadingLevel)'];
174+
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n)'];
189175
}
190176

191177
/**
@@ -271,7 +257,6 @@ class Dashboard extends DashboardLayoutMixin(
271257
wrapper.firstElementChild.toggleAttribute(attr, !!wrapper[attr]);
272258
});
273259
wrapper.firstElementChild.__i18n = this.__effectiveI18n;
274-
wrapper.firstElementChild.__rootHeadingLevel = this.rootHeadingLevel;
275260
}
276261
});
277262
}
@@ -315,7 +300,6 @@ class Dashboard extends DashboardLayoutMixin(
315300

316301
SYNCHRONIZED_ATTRIBUTES.forEach((attr) => section.toggleAttribute(attr, !!wrapper[attr]));
317302
section.__i18n = this.__effectiveI18n;
318-
section.__rootHeadingLevel = this.rootHeadingLevel;
319303

320304
// Render the subitems
321305
section.__childCount = item.items.length;
@@ -440,7 +424,6 @@ class Dashboard extends DashboardLayoutMixin(
440424
wrapper['first-child'] = item === getItemsArrayOfItem(item, this.items)[0];
441425
wrapper['last-child'] = item === getItemsArrayOfItem(item, this.items).slice(-1)[0];
442426
wrapper.i18n = this.__effectiveI18n;
443-
wrapper.__rootHeadingLevel = this.rootHeadingLevel;
444427
}
445428

446429
/** @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)