Skip to content

Commit c3a8661

Browse files
ugur-vaadinclaudesissbruecker
authored
feat: add support for slotted header and footer content (#10624)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Sascha Ißbrücker <sissbruecker@vaadin.com>
1 parent ae99286 commit c3a8661

9 files changed

Lines changed: 175 additions & 16 deletions

File tree

packages/dialog/src/vaadin-dialog-overlay-mixin.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Copyright (c) 2017 - 2026 Vaadin Ltd.
44
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
55
*/
6+
import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
67
import { OverlayMixin } from '@vaadin/overlay/src/vaadin-overlay-mixin.js';
78
import { setOverlayStateAttribute } from '@vaadin/overlay/src/vaadin-overlay-utils.js';
89

@@ -90,6 +91,19 @@ export const DialogOverlayMixin = (superClass) =>
9091
this.shadowRoot.addEventListener('slotchange', () => {
9192
this.__updateOverflow();
9293
});
94+
95+
// Observe header-content and footer slots for dynamic content
96+
const headerSlot = this.shadowRoot.querySelector('slot[name="header-content"]');
97+
this.__headerSlotObserver = new SlotObserver(headerSlot, ({ currentNodes }) => {
98+
setOverlayStateAttribute(this, 'has-header', currentNodes.length > 0);
99+
this.__updateOverflow();
100+
});
101+
102+
const footerSlot = this.shadowRoot.querySelector('slot[name="footer"]');
103+
this.__footerSlotObserver = new SlotObserver(footerSlot, ({ currentNodes }) => {
104+
setOverlayStateAttribute(this, 'has-footer', currentNodes.length > 0);
105+
this.__updateOverflow();
106+
});
93107
}
94108

95109
/** @private */
@@ -132,17 +146,12 @@ export const DialogOverlayMixin = (superClass) =>
132146
const openedChanged = this._oldOpenedFooterHeader !== opened;
133147
this._oldOpenedFooterHeader = opened;
134148

135-
// Set attributes here to update styles before detecting content overflow
136-
setOverlayStateAttribute(this, 'has-header', !!headerRenderer);
137-
setOverlayStateAttribute(this, 'has-footer', !!footerRenderer);
138-
139149
if (headerRendererChanged) {
140150
if (headerRenderer) {
141151
this.headerContainer = this.__initContainer(this.headerContainer, 'header-content');
142152
} else if (this.headerContainer) {
143153
this.headerContainer.remove();
144154
this.headerContainer = null;
145-
this.__updateOverflow();
146155
}
147156
}
148157

@@ -152,7 +161,6 @@ export const DialogOverlayMixin = (superClass) =>
152161
} else if (this.footerContainer) {
153162
this.footerContainer.remove();
154163
this.footerContainer = null;
155-
this.__updateOverflow();
156164
}
157165
}
158166

packages/dialog/test/dom/dialog.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ describe('vaadin-dialog', () => {
3737
dialog.headerRenderer = (root) => {
3838
root.textContent = 'Header';
3939
};
40-
await nextUpdate(dialog);
40+
await nextRender();
4141
await expect(dialog).dom.to.equalSnapshot(SNAPSHOT_CONFIG);
4242
});
4343

4444
it('footerRenderer', async () => {
4545
dialog.footerRenderer = (root) => {
4646
root.textContent = 'Footer';
4747
};
48-
await nextUpdate(dialog);
48+
await nextRender();
4949
await expect(dialog).dom.to.equalSnapshot(SNAPSHOT_CONFIG);
5050
});
5151
});

packages/dialog/test/header-footer.test.js

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('header/footer feature', () => {
207207
await nextRender();
208208

209209
dialog.headerRenderer = null;
210-
await nextUpdate(dialog);
210+
await nextRender();
211211
expect(dialog.hasAttribute('has-header')).to.be.not.ok;
212212
expect(overlay.hasAttribute('has-header')).to.be.not.ok;
213213
});
@@ -293,7 +293,7 @@ describe('header/footer feature', () => {
293293
await nextRender();
294294

295295
dialog.footerRenderer = null;
296-
await nextUpdate(dialog);
296+
await nextRender();
297297
expect(dialog.hasAttribute('has-footer')).to.be.not.ok;
298298
expect(overlay.hasAttribute('has-footer')).to.be.not.ok;
299299
});
@@ -447,7 +447,7 @@ describe('header/footer feature', () => {
447447
expect(getComputedStyle(headerPart).display).to.not.be.equal('none');
448448

449449
dialog.headerTitle = null;
450-
await nextUpdate(dialog);
450+
await nextRender();
451451
expect(getComputedStyle(headerPart).display).to.not.be.equal('none');
452452
});
453453

@@ -462,7 +462,7 @@ describe('header/footer feature', () => {
462462
expect(getComputedStyle(headerPart).display).to.not.be.equal('none');
463463

464464
dialog.headerRenderer = null;
465-
await nextUpdate(dialog);
465+
await nextRender();
466466
expect(getComputedStyle(headerPart).display).to.not.be.equal('none');
467467
});
468468

@@ -478,7 +478,7 @@ describe('header/footer feature', () => {
478478

479479
dialog.headerTitle = null;
480480
dialog.headerRenderer = null;
481-
await nextUpdate(dialog);
481+
await nextRender();
482482
expect(getComputedStyle(headerPart).display).to.be.equal('none');
483483
});
484484
});
@@ -536,3 +536,107 @@ describe('renderer set before attach', () => {
536536
expect(spy.calledOnce).to.be.true;
537537
});
538538
});
539+
540+
describe('slotted header content', () => {
541+
let dialog, overlay;
542+
543+
beforeEach(async () => {
544+
dialog = fixtureSync('<vaadin-dialog></vaadin-dialog>');
545+
await nextRender();
546+
overlay = dialog.$.overlay;
547+
});
548+
549+
afterEach(async () => {
550+
dialog.opened = false;
551+
await nextRender();
552+
});
553+
554+
it('should not have [has-header] attribute when no header content is slotted', async () => {
555+
dialog.opened = true;
556+
await nextRender();
557+
expect(dialog.hasAttribute('has-header')).to.be.false;
558+
expect(overlay.hasAttribute('has-header')).to.be.false;
559+
});
560+
561+
it('should set [has-header] attribute when header content is slotted', async () => {
562+
const header = document.createElement('div');
563+
header.setAttribute('slot', 'header-content');
564+
header.textContent = 'Header Content';
565+
dialog.appendChild(header);
566+
567+
dialog.opened = true;
568+
await nextRender();
569+
570+
expect(dialog.hasAttribute('has-header')).to.be.true;
571+
expect(overlay.hasAttribute('has-header')).to.be.true;
572+
});
573+
574+
it('should remove [has-header] attribute when slotted header content is removed', async () => {
575+
const header = document.createElement('div');
576+
header.setAttribute('slot', 'header-content');
577+
header.textContent = 'Header Content';
578+
dialog.appendChild(header);
579+
580+
dialog.opened = true;
581+
await nextRender();
582+
expect(overlay.hasAttribute('has-header')).to.be.true;
583+
584+
header.remove();
585+
await nextRender();
586+
587+
expect(dialog.hasAttribute('has-header')).to.be.false;
588+
expect(overlay.hasAttribute('has-header')).to.be.false;
589+
});
590+
});
591+
592+
describe('slotted footer content', () => {
593+
let dialog, overlay;
594+
595+
beforeEach(async () => {
596+
dialog = fixtureSync('<vaadin-dialog></vaadin-dialog>');
597+
await nextRender();
598+
overlay = dialog.$.overlay;
599+
});
600+
601+
afterEach(async () => {
602+
dialog.opened = false;
603+
await nextRender();
604+
});
605+
606+
it('should not have [has-footer] attribute when no footer content is slotted', async () => {
607+
dialog.opened = true;
608+
await nextRender();
609+
expect(dialog.hasAttribute('has-footer')).to.be.false;
610+
expect(overlay.hasAttribute('has-footer')).to.be.false;
611+
});
612+
613+
it('should set [has-footer] attribute when footer content is slotted', async () => {
614+
const footer = document.createElement('div');
615+
footer.setAttribute('slot', 'footer');
616+
footer.textContent = 'Footer Content';
617+
dialog.appendChild(footer);
618+
619+
dialog.opened = true;
620+
await nextRender();
621+
622+
expect(dialog.hasAttribute('has-footer')).to.be.true;
623+
expect(overlay.hasAttribute('has-footer')).to.be.true;
624+
});
625+
626+
it('should remove [has-footer] attribute when slotted footer content is removed', async () => {
627+
const footer = document.createElement('div');
628+
footer.setAttribute('slot', 'footer');
629+
footer.textContent = 'Footer Content';
630+
dialog.appendChild(footer);
631+
632+
dialog.opened = true;
633+
await nextRender();
634+
expect(overlay.hasAttribute('has-footer')).to.be.true;
635+
636+
footer.remove();
637+
await nextRender();
638+
639+
expect(dialog.hasAttribute('has-footer')).to.be.false;
640+
expect(overlay.hasAttribute('has-footer')).to.be.false;
641+
});
642+
});

packages/dialog/test/overflow.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe('overflow', () => {
120120
expect(overlay.hasAttribute('overflow')).to.be.true;
121121

122122
dialog.headerRenderer = null;
123-
await nextUpdate(dialog);
123+
await nextRender();
124124
expect(dialog.hasAttribute('overflow')).to.be.false;
125125
expect(overlay.hasAttribute('overflow')).to.be.false;
126126
});
@@ -133,7 +133,7 @@ describe('overflow', () => {
133133
expect(overlay.hasAttribute('overflow')).to.be.true;
134134

135135
dialog.footerRenderer = null;
136-
await nextUpdate(dialog);
136+
await nextRender();
137137
expect(dialog.hasAttribute('overflow')).to.be.false;
138138
expect(overlay.hasAttribute('overflow')).to.be.false;
139139
});
@@ -182,7 +182,7 @@ describe('overflow', () => {
182182
dialog.headerRenderer = null;
183183
dialog.footerRenderer = null;
184184
dialog.headerTitle = null;
185-
await nextUpdate(dialog);
185+
await nextRender();
186186
expect(dialog.hasAttribute('overflow')).to.be.false;
187187
expect(overlay.hasAttribute('overflow')).to.be.false;
188188
});

packages/dialog/test/visual/base/dialog.test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,51 @@ describe('dialog', () => {
8383
await nextUpdate(element);
8484
await visualDiff(div, 'content-no-padding-theme');
8585
});
86+
87+
describe('slotted content', () => {
88+
it('slotted header', async () => {
89+
const header = document.createElement('div');
90+
header.setAttribute('slot', 'header-content');
91+
header.textContent = 'Slotted Header';
92+
element.appendChild(header);
93+
await nextUpdate(element);
94+
await visualDiff(div, 'slotted-header');
95+
});
96+
97+
it('slotted footer', async () => {
98+
const footer = document.createElement('div');
99+
footer.setAttribute('slot', 'footer');
100+
footer.textContent = 'Slotted Footer';
101+
element.appendChild(footer);
102+
await nextUpdate(element);
103+
await visualDiff(div, 'slotted-footer');
104+
});
105+
106+
it('slotted header and footer', async () => {
107+
const header = document.createElement('div');
108+
header.setAttribute('slot', 'header-content');
109+
header.textContent = 'Slotted Header';
110+
element.appendChild(header);
111+
112+
const footer = document.createElement('div');
113+
footer.setAttribute('slot', 'footer');
114+
footer.textContent = 'Slotted Footer';
115+
element.appendChild(footer);
116+
117+
await nextUpdate(element);
118+
await visualDiff(div, 'slotted-header-footer');
119+
});
120+
121+
it('title with slotted header', async () => {
122+
element.headerTitle = 'Dialog Title';
123+
124+
const header = document.createElement('div');
125+
header.setAttribute('slot', 'header-content');
126+
header.textContent = 'Close';
127+
element.appendChild(header);
128+
129+
await nextUpdate(element);
130+
await visualDiff(div, 'title-slotted-header');
131+
});
132+
});
86133
});
9.39 KB
Loading
10.7 KB
Loading
9.58 KB
Loading
9.91 KB
Loading

0 commit comments

Comments
 (0)