Skip to content

feat: add root heading level support to dashboard layout #9249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions packages/dashboard/src/vaadin-dashboard-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,10 @@ export function fireRemove(element) {
element.dispatchEvent(new CustomEvent('item-remove', { bubbles: true }));
}

/**
* Walks up the DOM tree starting from `node`, returning the first ancestor which is an instance of the given `baseClass`.
*
* @param {Node} node - starting node
* @param {Function} baseClass - constructor, e.g. `Dashboard`
* @returns {HTMLElement | null}
*/
export function findAncestorInstance(node, baseClass) {
/** @private */
function __findFilteredAncestorInstance(node, elementFilter) {
while (node) {
if (node instanceof baseClass) {
if (elementFilter(node)) {
return node;
}
if (node instanceof ShadowRoot) {
Expand All @@ -129,3 +123,30 @@ export function findAncestorInstance(node, baseClass) {
}
return null;
}

/**
* Walks up the DOM tree starting from `node`, returning the first ancestor which is an instance of the given `baseClass`.
*
* @param {Node} node - starting node
* @param {Function} baseClass - constructor, e.g. `Dashboard`
* @returns {HTMLElement | null}
*/
export function findAncestorInstance(node, baseClass) {
return __findFilteredAncestorInstance(node, (el) => {
return el instanceof baseClass;
});
}

/**
* Walks up the DOM tree starting from `node`, returning the first ancestor which is a `Dashboard` or `DashboardLayout`.
*
* @param {Node} node - starting node
* @returns {HTMLElement | null}
*/
export function getParentLayout(node) {
return __findFilteredAncestorInstance(node, (el) => {
return (
el.constructor && (el.constructor.is === 'vaadin-dashboard' || el.constructor.is === 'vaadin-dashboard-layout')
);
});
}
53 changes: 52 additions & 1 deletion packages/dashboard/src/vaadin-dashboard-item-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { html } from 'lit';
import { FocusTrapController } from '@vaadin/a11y-base/src/focus-trap-controller.js';
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
import { KeyboardController } from './keyboard-controller.js';
import { fireMove, fireRemove, fireResize } from './vaadin-dashboard-helpers.js';
import { fireMove, fireRemove, fireResize, getParentLayout } from './vaadin-dashboard-helpers.js';
import { dashboardWidgetAndSectionStyles } from './vaadin-dashboard-styles.js';

const DEFAULT_I18N = {
Expand Down Expand Up @@ -58,6 +58,11 @@ export const DashboardItemMixin = (superClass) =>
type: Object,
},

/** @protected */
_rootHeadingLevel: {
type: Number,
},

/** @private */
__selected: {
type: Boolean,
Expand Down Expand Up @@ -288,6 +293,7 @@ export const DashboardItemMixin = (superClass) =>
super();
this.__keyboardController = new KeyboardController(this);
this.__focusTrapController = new FocusTrapController(this);
this.__boundRootHeadingLevelChangedListener = this.__updateRootHeadingLevel.bind(this);
}

/** @protected */
Expand All @@ -297,6 +303,19 @@ export const DashboardItemMixin = (superClass) =>
this.addController(this.__focusTrapController);
}

/** @protected */
connectedCallback() {
super.connectedCallback();
this.__updateRootHeadingLevel();
this.__setupHeadingLevelObserver();
}

/** @protected */
disconnectedCallback() {
super.disconnectedCallback();
this.__removeHeadingLevelObserver();
}

/** @private */
__selectedChanged(selected, oldSelected) {
if (!!selected === !!oldSelected) {
Expand Down Expand Up @@ -377,4 +396,36 @@ export const DashboardItemMixin = (superClass) =>
}
this.dispatchEvent(new CustomEvent('item-resize-mode-changed', { bubbles: true, detail: { value: resizeMode } }));
}

/** @private */
__setupHeadingLevelObserver() {
this.__removeHeadingLevelObserver();
const parentLayout = getParentLayout(this);
if (parentLayout) {
this.__rootHeadingLevelListenerTarget = parentLayout;
parentLayout.addEventListener(
'dashboard-root-heading-level-changed',
this.__boundRootHeadingLevelChangedListener,
);
}
}

/** @private */
__removeHeadingLevelObserver() {
if (this.__rootHeadingLevelListenerTarget) {
this.__rootHeadingLevelListenerTarget.removeEventListener(
'dashboard-root-heading-level-changed',
this.__boundRootHeadingLevelChangedListener,
);
this.__rootHeadingLevelListenerTarget = null;
}
}

/** @private */
__updateRootHeadingLevel() {
const parentLayout = getParentLayout(this);
if (parentLayout) {
this._rootHeadingLevel = parentLayout.rootHeadingLevel;
}
}
};
10 changes: 10 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-layout-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,14 @@ export declare class DashboardLayoutMixinClass {
* @attr {boolean} dense-layout
*/
denseLayout: boolean;

/**
* Root heading level for sections and widgets. Defaults to 2.
*
* If changed to e.g. 1:
* - sections will have the attribute `aria-level` with value 1
* - non-nested widgets will have the attribute `aria-level` with value 1
* - nested widgets will have the attribute `aria-level` with value 2
*/
rootHeadingLevel: number | null | undefined;
}
23 changes: 23 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-layout-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ export const DashboardLayoutMixin = (superClass) =>
value: false,
reflectToAttribute: true,
},

/**
* Root heading level for sections and widgets. Defaults to 2.
*
* If changed to e.g. 1:
* - sections will have the attribute `aria-level` with value 1
* - non-nested widgets will have the attribute `aria-level` with value 1
* - nested widgets will have the attribute `aria-level` with value 2
*/
rootHeadingLevel: {
type: Number,
value: 2,
sync: true,
reflectToAttribute: true,
observer: '__rootHeadingLevelChanged',
},
};
}

Expand Down Expand Up @@ -141,4 +157,11 @@ export const DashboardLayoutMixin = (superClass) =>
// ...and set it as the new value
this.$.grid.style.setProperty('--_col-count', columnCount);
}

/** @private */
__rootHeadingLevelChanged(rootHeadingLevel) {
this.dispatchEvent(
new CustomEvent('dashboard-root-heading-level-changed', { detail: { value: rootHeadingLevel } }),
);
}
};
7 changes: 1 addition & 6 deletions packages/dashboard/src/vaadin-dashboard-section.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,6 @@ class DashboardSection extends DashboardItemMixin(ElementMixin(ThemableMixin(Pol
value: '',
},

/* @private */
__rootHeadingLevel: {
type: Number,
},

/** @private */
__childCount: {
type: Number,
Expand All @@ -178,7 +173,7 @@ class DashboardSection extends DashboardItemMixin(ElementMixin(ThemableMixin(Pol

<header part="header">
${this.__renderDragHandle()}
<div id="title" role="heading" aria-level=${this.__rootHeadingLevel || 2} part="title"
<div id="title" role="heading" aria-level=${this._rootHeadingLevel || 2} part="title"
>${this.sectionTitle}</div
>
${this.__renderRemoveButton()}
Expand Down
8 changes: 1 addition & 7 deletions packages/dashboard/src/vaadin-dashboard-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,6 @@ class DashboardWidget extends DashboardItemMixin(ElementMixin(ThemableMixin(Poly
value: '',
},

/* @private */
__rootHeadingLevel: {
type: Number,
},

/* @private */
__isNestedWidget: {
type: Boolean,
Expand Down Expand Up @@ -245,7 +240,6 @@ class DashboardWidget extends DashboardItemMixin(ElementMixin(ThemableMixin(Poly
this.toggleAttribute(attr, !!wrapper[attr]);
});
this.__i18n = wrapper.i18n;
this.__rootHeadingLevel = wrapper.__rootHeadingLevel;
}

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

/** @private */
__renderWidgetTitle() {
let effectiveHeadingLevel = this.__rootHeadingLevel;
let effectiveHeadingLevel = this._rootHeadingLevel;
// Default to 2 if not defined
if (effectiveHeadingLevel == null) {
effectiveHeadingLevel = 2;
Expand Down
10 changes: 0 additions & 10 deletions packages/dashboard/src/vaadin-dashboard.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,16 +243,6 @@ declare class Dashboard<TItem extends DashboardItem = DashboardItem> extends Das
*/
editable: boolean;

/**
* Root heading level for sections and widgets. Defaults to 2.
*
* If changed to e.g. 1:
* - sections will have the attribute `aria-level` with value 1
* - non-nested widgets will have the attribute `aria-level` with value 1
* - nested widgets will have the attribute `aria-level` with value 2
*/
rootHeadingLevel: number | null | undefined;

/**
* The object used to localize this component. To change the default
* localization, replace this with an object that provides all properties, or
Expand Down
19 changes: 1 addition & 18 deletions packages/dashboard/src/vaadin-dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,20 +162,6 @@ class Dashboard extends DashboardLayoutMixin(
type: Boolean,
},

/**
* Root heading level for sections and widgets. Defaults to 2.
*
* If changed to e.g. 1:
* - sections will have the attribute `aria-level` with value 1
* - non-nested widgets will have the attribute `aria-level` with value 1
* - nested widgets will have the attribute `aria-level` with value 2
*/
rootHeadingLevel: {
type: Number,
value: 2,
sync: true,
},

/** @private */
__childCount: {
type: Number,
Expand All @@ -185,7 +171,7 @@ class Dashboard extends DashboardLayoutMixin(
}

static get observers() {
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n, rootHeadingLevel)'];
return ['__itemsOrRendererChanged(items, renderer, editable, __effectiveI18n)'];
}

/**
Expand Down Expand Up @@ -271,7 +257,6 @@ class Dashboard extends DashboardLayoutMixin(
wrapper.firstElementChild.toggleAttribute(attr, !!wrapper[attr]);
});
wrapper.firstElementChild.__i18n = this.__effectiveI18n;
wrapper.firstElementChild.__rootHeadingLevel = this.rootHeadingLevel;
}
});
}
Expand Down Expand Up @@ -315,7 +300,6 @@ class Dashboard extends DashboardLayoutMixin(

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

// Render the subitems
section.__childCount = item.items.length;
Expand Down Expand Up @@ -440,7 +424,6 @@ class Dashboard extends DashboardLayoutMixin(
wrapper['first-child'] = item === getItemsArrayOfItem(item, this.items)[0];
wrapper['last-child'] = item === getItemsArrayOfItem(item, this.items).slice(-1)[0];
wrapper.i18n = this.__effectiveI18n;
wrapper.__rootHeadingLevel = this.rootHeadingLevel;
}

/** @private */
Expand Down
Loading