From 158e64d15c7c58623f42c24615c7503d0be5b76a Mon Sep 17 00:00:00 2001 From: Tomi Virkki Date: Wed, 4 Sep 2024 09:03:42 +0300 Subject: [PATCH] feat: auto-update widget title level (#7740) --- packages/dashboard/src/title-controller.js | 14 +- .../dashboard/src/vaadin-dashboard-section.js | 1 + .../dashboard/src/vaadin-dashboard-widget.js | 21 ++- .../dashboard/test/dashboard-widget.test.ts | 134 +++++++++++++++++- 4 files changed, 166 insertions(+), 4 deletions(-) diff --git a/packages/dashboard/src/title-controller.js b/packages/dashboard/src/title-controller.js index 5577d23a8f..7549cc1dd1 100644 --- a/packages/dashboard/src/title-controller.js +++ b/packages/dashboard/src/title-controller.js @@ -5,6 +5,8 @@ */ import { SlotChildObserveController } from '@vaadin/component-base/src/slot-child-observe-controller.js'; +const DEFAULT_TITLE_LEVEL = '2'; + /** * A controller to manage the widget or section title element. */ @@ -21,6 +23,17 @@ export class TitleController extends SlotChildObserveController { setTitle(title) { this.title = title; + const titleLevel = + getComputedStyle(this.host).getPropertyValue('--_vaadin-dashboard-title-level') || DEFAULT_TITLE_LEVEL; + const newTagName = `h${titleLevel}`; + if (this.tagName !== newTagName) { + if (this.defaultNode) { + this.defaultNode.remove(); + delete this.defaultNode; + } + this.tagName = newTagName; + } + // Restore the default title, if needed. const titleNode = this.getSlotChild(); if (!titleNode) { @@ -41,7 +54,6 @@ export class TitleController extends SlotChildObserveController { * @override */ restoreDefaultNode() { - this.tagName = 'h2'; this.attachDefaultNode(); } diff --git a/packages/dashboard/src/vaadin-dashboard-section.js b/packages/dashboard/src/vaadin-dashboard-section.js index 949f9195b8..21e5679066 100644 --- a/packages/dashboard/src/vaadin-dashboard-section.js +++ b/packages/dashboard/src/vaadin-dashboard-section.js @@ -44,6 +44,7 @@ class DashboardSection extends ControllerMixin(ElementMixin(PolylitMixin(LitElem } ::slotted(*) { + --_vaadin-dashboard-title-level: 3; --_vaadin-dashboard-item-column: span min( var(--vaadin-dashboard-item-colspan, 1), diff --git a/packages/dashboard/src/vaadin-dashboard-widget.js b/packages/dashboard/src/vaadin-dashboard-widget.js index 8fcabbcb0d..661aa40f16 100644 --- a/packages/dashboard/src/vaadin-dashboard-widget.js +++ b/packages/dashboard/src/vaadin-dashboard-widget.js @@ -92,6 +92,18 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme }); } + /** @protected */ + connectedCallback() { + super.connectedCallback(); + + const undefinedAncestor = this.closest('*:not(:defined)'); + if (undefinedAncestor) { + customElements.whenDefined(undefinedAncestor.localName).then(() => queueMicrotask(() => this.__updateTitle())); + } else { + this.__updateTitle(); + } + } + /** @protected */ ready() { super.ready(); @@ -103,8 +115,13 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme } /** @private */ - __onWidgetTitleChanged(widgetTitle) { - this.__titleController.setTitle(widgetTitle); + __onWidgetTitleChanged() { + this.__updateTitle(); + } + + /** @private */ + __updateTitle() { + this.__titleController.setTitle(this.widgetTitle); } } diff --git a/packages/dashboard/test/dashboard-widget.test.ts b/packages/dashboard/test/dashboard-widget.test.ts index 96e5c83fe3..fccf788b72 100644 --- a/packages/dashboard/test/dashboard-widget.test.ts +++ b/packages/dashboard/test/dashboard-widget.test.ts @@ -1,7 +1,8 @@ import { expect } from '@vaadin/chai-plugins'; import { fixtureSync, nextFrame } from '@vaadin/testing-helpers'; import '../vaadin-dashboard-widget.js'; -import type { DashboardWidget } from '../vaadin-dashboard-widget.js'; +import { DashboardSection } from '../vaadin-dashboard-section.js'; +import { DashboardWidget } from '../vaadin-dashboard-widget.js'; describe('dashboard widget', () => { let widget: DashboardWidget; @@ -95,3 +96,134 @@ describe('dashboard widget', () => { }); }); }); + +describe('widget title level', () => { + it('should have h2 title by default', async () => { + const widget = fixtureSync(``); + await nextFrame(); + + const title = widget.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h2'); + }); + + it('should have h2 title by default on the section', async () => { + const section = fixtureSync(``); + await nextFrame(); + + const title = section.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h2'); + }); + + it('should have h3 title when rendered inside a section', async () => { + const widget = fixtureSync(` + + + + `).querySelector('vaadin-dashboard-widget')!; + await nextFrame(); + + const title = widget.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h3'); + }); + + it('should have h2 title after moving out of a section', async () => { + const widget = fixtureSync(` +
+ + + +
+ `).querySelector('vaadin-dashboard-widget')!; + await nextFrame(); + + const wrapper = widget.closest('div')!; + wrapper.appendChild(widget); + await nextFrame(); + + const title = widget.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h2'); + }); + + it('should have h3 title after moving into a section', async () => { + const widget = fixtureSync(` +
+ + +
+ `).querySelector('vaadin-dashboard-widget')!; + await nextFrame(); + + const section = widget.nextElementSibling as DashboardSection; + section.appendChild(widget); + await nextFrame(); + + const title = widget.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h3'); + }); + + it('should have h3 title after defining parent section', async () => { + const widget = fixtureSync(` + + + + `).querySelector('vaadin-dashboard-widget')!; + await nextFrame(); + + class MyCustomSection extends DashboardSection {} + customElements.define('my-custom-section', MyCustomSection); + await nextFrame(); + + const title = widget.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h3'); + }); + + it('should have h3 title after defining the widget', async () => { + const widget = fixtureSync(` + + + + `).querySelector('my-custom-widget')!; + await nextFrame(); + + class MyCustomWidget extends DashboardWidget {} + customElements.define('my-custom-widget', MyCustomWidget); + await nextFrame(); + + const title = widget.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h3'); + }); + + it('should have h3 title after moving a wrapped widget into a section', async () => { + const widget = fixtureSync(` +
+
+ +
+ +
+ `).querySelector('vaadin-dashboard-widget')!; + await nextFrame(); + + const wrapper = widget.closest('div#wrapper')!; + const section = wrapper.nextElementSibling as DashboardSection; + section.appendChild(wrapper); + await nextFrame(); + + const title = widget.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h3'); + }); + + it('should not replace an explicitly defined widget title element', async () => { + const widget = fixtureSync(` + + +

foo

+
+
+ `).querySelector('vaadin-dashboard-widget')!; + await nextFrame(); + + const title = widget.querySelector('[slot="title"]'); + expect(title?.localName).to.equal('h2'); + }); +});