Skip to content

Commit 05a86e6

Browse files
feat: add root heading level support to dashboard layout (#9249)
* feat: add root level heading support to dashboard layout * refactor: use is to determine whether the element is a dashboard * test: remove duplicate test * test: move section tests, remove duplicates, make all elements custom * Update packages/dashboard/test/dashboard-layout.test.ts Co-authored-by: Serhii Kulykov <iamkulykov@gmail.com> * Update packages/dashboard/test/dashboard-layout.test.ts Co-authored-by: Serhii Kulykov <iamkulykov@gmail.com> * refactor: use event instead of mutation observer * test: update snapshot tests * refactor: add private property for detecting mixin * chore: remove private marking * chore: remove double underscore from function name * chore: add missing attr JSDoc annotations * chore: do not declare private property for mixin detection flag * chore: rename method to follow add / remove convention * chore: rename methods to not use observer --------- Co-authored-by: Serhii Kulykov <iamkulykov@gmail.com>
1 parent acf5233 commit 05a86e6

15 files changed

+270
-83
lines changed

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

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,9 @@ 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+
function findFilteredAncestorInstance(node, elementFilter) {
113106
while (node) {
114-
if (node instanceof baseClass) {
107+
if (elementFilter(node)) {
115108
return node;
116109
}
117110
if (node instanceof ShadowRoot) {
@@ -129,3 +122,26 @@ export function findAncestorInstance(node, baseClass) {
129122
}
130123
return null;
131124
}
125+
126+
/**
127+
* Walks up the DOM tree starting from `node`, returning the first ancestor which is an instance of the given `baseClass`.
128+
*
129+
* @param {Node} node - starting node
130+
* @param {Function} baseClass - constructor, e.g. `Dashboard`
131+
* @returns {HTMLElement | null}
132+
*/
133+
export function findAncestorInstance(node, baseClass) {
134+
return findFilteredAncestorInstance(node, (el) => {
135+
return el instanceof baseClass;
136+
});
137+
}
138+
139+
/**
140+
* Walks up the DOM tree starting from `node`, returning the first ancestor which is a `Dashboard` or `DashboardLayout`.
141+
*
142+
* @param {Node} node - starting node
143+
* @returns {HTMLElement | null}
144+
*/
145+
export function getParentLayout(node) {
146+
return findFilteredAncestorInstance(node, (el) => el.__hasVaadinDashboardLayoutMixin);
147+
}

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

Lines changed: 52 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,
@@ -283,6 +288,7 @@ export const DashboardItemMixin = (superClass) =>
283288
super();
284289
this.__keyboardController = new KeyboardController(this);
285290
this.__focusTrapController = new FocusTrapController(this);
291+
this.__boundRootHeadingLevelChangedListener = this.__updateRootHeadingLevel.bind(this);
286292
}
287293

288294
/** @protected */
@@ -292,6 +298,19 @@ export const DashboardItemMixin = (superClass) =>
292298
this.addController(this.__focusTrapController);
293299
}
294300

301+
/** @protected */
302+
connectedCallback() {
303+
super.connectedCallback();
304+
this.__updateRootHeadingLevel();
305+
this.__addHeadingLevelListener();
306+
}
307+
308+
/** @protected */
309+
disconnectedCallback() {
310+
super.disconnectedCallback();
311+
this.__removeHeadingLevelListener();
312+
}
313+
295314
/** @private */
296315
__selectedChanged(selected, oldSelected) {
297316
if (!!selected === !!oldSelected) {
@@ -377,4 +396,36 @@ export const DashboardItemMixin = (superClass) =>
377396
}
378397
this.dispatchEvent(new CustomEvent('item-resize-mode-changed', { bubbles: true, detail: { value: resizeMode } }));
379398
}
399+
400+
/** @private */
401+
__addHeadingLevelListener() {
402+
this.__removeHeadingLevelListener();
403+
const parentLayout = getParentLayout(this);
404+
if (parentLayout) {
405+
this.__rootHeadingLevelListenerTarget = parentLayout;
406+
parentLayout.addEventListener(
407+
'dashboard-root-heading-level-changed',
408+
this.__boundRootHeadingLevelChangedListener,
409+
);
410+
}
411+
}
412+
413+
/** @private */
414+
__removeHeadingLevelListener() {
415+
if (this.__rootHeadingLevelListenerTarget) {
416+
this.__rootHeadingLevelListenerTarget.removeEventListener(
417+
'dashboard-root-heading-level-changed',
418+
this.__boundRootHeadingLevelChangedListener,
419+
);
420+
this.__rootHeadingLevelListenerTarget = null;
421+
}
422+
}
423+
424+
/** @private */
425+
__updateRootHeadingLevel() {
426+
const parentLayout = getParentLayout(this);
427+
if (parentLayout) {
428+
this._rootHeadingLevel = parentLayout.rootHeadingLevel;
429+
}
430+
}
380431
};

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,16 @@ 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+
* @attr {number} root-heading-level
38+
*/
39+
rootHeadingLevel: number | null | undefined;
2840
}

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,36 @@ 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+
* @attr {number} root-heading-level
44+
*/
45+
rootHeadingLevel: {
46+
type: Number,
47+
value: 2,
48+
sync: true,
49+
reflectToAttribute: true,
50+
observer: '__rootHeadingLevelChanged',
51+
},
3452
};
3553
}
3654

55+
constructor() {
56+
super();
57+
58+
/**
59+
* Used for mixin detection because `instanceof` does not work with mixins.
60+
*/
61+
this.__hasVaadinDashboardLayoutMixin = true;
62+
}
63+
3764
/** @protected */
3865
ready() {
3966
super.ready();
@@ -63,4 +90,11 @@ export const DashboardLayoutMixin = (superClass) =>
6390
// ...and set it as the new value
6491
this.$.grid.style.setProperty('--_col-count', columnCount);
6592
}
93+
94+
/** @private */
95+
__rootHeadingLevelChanged(rootHeadingLevel) {
96+
this.dispatchEvent(
97+
new CustomEvent('dashboard-root-heading-level-changed', { detail: { value: rootHeadingLevel } }),
98+
);
99+
}
66100
};

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

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

127-
/* @private */
128-
__rootHeadingLevel: {
129-
type: Number,
130-
},
131-
132127
/** @private */
133128
__childCount: {
134129
type: Number,
@@ -147,7 +142,7 @@ class DashboardSection extends DashboardItemMixin(
147142
148143
<header part="header">
149144
${this.__renderDragHandle()}
150-
<div id="title" role="heading" aria-level=${this.__rootHeadingLevel || 2} part="title"
145+
<div id="title" role="heading" aria-level=${this._rootHeadingLevel || 2} part="title"
151146
>${this.sectionTitle}</div
152147
>
153148
${this.__renderRemoveButton()}

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

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

167-
/* @private */
168-
__rootHeadingLevel: {
169-
type: Number,
170-
},
171-
172167
/* @private */
173168
__isNestedWidget: {
174169
type: Boolean,
@@ -210,7 +205,6 @@ class DashboardWidget extends DashboardItemMixin(
210205
this.toggleAttribute(attr, !!wrapper[attr]);
211206
});
212207
this.__i18n = wrapper.i18n;
213-
this.__rootHeadingLevel = wrapper.__rootHeadingLevel;
214208
}
215209

216210
this.__updateNestedState();
@@ -232,7 +226,7 @@ class DashboardWidget extends DashboardItemMixin(
232226

233227
/** @private */
234228
__renderWidgetTitle() {
235-
let effectiveHeadingLevel = this.__rootHeadingLevel;
229+
let effectiveHeadingLevel = this._rootHeadingLevel;
236230
// Default to 2 if not defined
237231
if (effectiveHeadingLevel == null) {
238232
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
@@ -158,20 +158,6 @@ class Dashboard extends DashboardLayoutMixin(
158158
type: Boolean,
159159
},
160160

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

183169
static get observers() {
184-
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n, rootHeadingLevel)'];
170+
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n)'];
185171
}
186172

187173
/**
@@ -267,7 +253,6 @@ class Dashboard extends DashboardLayoutMixin(
267253
wrapper.firstElementChild.toggleAttribute(attr, !!wrapper[attr]);
268254
});
269255
wrapper.firstElementChild.__i18n = this.__effectiveI18n;
270-
wrapper.firstElementChild.__rootHeadingLevel = this.rootHeadingLevel;
271256
}
272257
});
273258
}
@@ -311,7 +296,6 @@ class Dashboard extends DashboardLayoutMixin(
311296

312297
SYNCHRONIZED_ATTRIBUTES.forEach((attr) => section.toggleAttribute(attr, !!wrapper[attr]));
313298
section.__i18n = this.__effectiveI18n;
314-
section.__rootHeadingLevel = this.rootHeadingLevel;
315299

316300
// Render the subitems
317301
section.__childCount = item.items.length;
@@ -436,7 +420,6 @@ class Dashboard extends DashboardLayoutMixin(
436420
wrapper['first-child'] = item === getItemsArrayOfItem(item, this.items)[0];
437421
wrapper['last-child'] = item === getItemsArrayOfItem(item, this.items).slice(-1)[0];
438422
wrapper.i18n = this.__effectiveI18n;
439-
wrapper.__rootHeadingLevel = this.rootHeadingLevel;
440423
}
441424

442425
/** @private */

0 commit comments

Comments
 (0)