Skip to content
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
107 changes: 67 additions & 40 deletions packages/card/src/vaadin-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,39 @@
*/
import { html, LitElement } from 'lit';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { isEmptyTextNode } from '@vaadin/component-base/src/dom-utils.js';
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { SlotObserver } from '@vaadin/component-base/src/slot-observer.js';
import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
import { cardStyles } from './styles/vaadin-card-base-styles.js';

/**
* A slot observer that toggles an attribute on the card host
* based on the presence of slotted content.
*
* @private
*/
class CardSlotObserver extends SlotObserver {
constructor(card, slotName, attr, callback) {
const slot = slotName
? card.shadowRoot.querySelector(`slot[name="${slotName}"]`)
: card.shadowRoot.querySelector('slot:not([name])');

super(slot, () => {
const nodes = slot.assignedNodes().filter((node) => !isEmptyTextNode(node));
if (attr) {
card.toggleAttribute(attr, nodes.length > 0);
}
if (callback) {
callback(nodes);
}
});
}
}

/**
* `<vaadin-card>` is a versatile container for grouping related content and actions.
* It presents information in a structured and visually appealing manner, with
Expand Down Expand Up @@ -103,17 +129,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
};
}

/** @protected */
ready() {
super.ready();

// By default, if the user hasn't provided a custom role,
// the role attribute is set to "region".
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'region');
}
}

/** @protected */
render() {
return html`
Expand All @@ -140,28 +155,50 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
/** @protected */
firstUpdated() {
super.firstUpdated();
this._onSlotChange();

// By default, if the user hasn't provided a custom role,
// the role attribute is set to "region".
if (!this.hasAttribute('role')) {
this.setAttribute('role', 'region');
}

this.__headerNodes = [];
this.__titleNodes = [];
this.__subtitleNodes = [];

const observers = [
new CardSlotObserver(this, 'media', '_m'),
new CardSlotObserver(this, 'header-prefix', '_hp'),
new CardSlotObserver(this, 'header', '_h', (nodes) => {
this.__headerNodes = nodes;
this.__updateHeaderAttributes();
}),
new CardSlotObserver(this, 'header-suffix', '_hs'),
new CardSlotObserver(this, '', '_c'),
new CardSlotObserver(this, 'footer', '_f'),
new CardSlotObserver(this, 'title', null, (nodes) => {
this.__titleNodes = nodes;
this.__updateHeaderAttributes();
if (this.__getCustomTitleElement()) {
this.__clearStringTitle();
}
}),
new CardSlotObserver(this, 'subtitle', null, (nodes) => {
this.__subtitleNodes = nodes;
this.__updateHeaderAttributes();
}),
];

// Flush observers synchronously to ensure attributes are set
// before the first render completes, enabling synchronous sizing.
observers.forEach((observer) => observer.flush());
}

/** @private */
_onSlotChange() {
this.toggleAttribute('_m', this.querySelector(':scope > [slot="media"]'));
this.toggleAttribute('_h', this.querySelector(':scope > [slot="header"]'));
this.toggleAttribute(
'_t',
this.querySelector(':scope > [slot="title"]') && !this.querySelector(':scope > [slot="header"]'),
);
this.toggleAttribute(
'_st',
this.querySelector(':scope > [slot="subtitle"]') && !this.querySelector(':scope > [slot="header"]'),
);
this.toggleAttribute('_hp', this.querySelector(':scope > [slot="header-prefix"]'));
this.toggleAttribute('_hs', this.querySelector(':scope > [slot="header-suffix"]'));
this.toggleAttribute('_c', this.querySelector(':scope > :not([slot])'));
this.toggleAttribute('_f', this.querySelector(':scope > [slot="footer"]'));
if (this.__getCustomTitleElement()) {
this.__clearStringTitle();
}
__updateHeaderAttributes() {
const hasHeader = this.__headerNodes.length > 0;
this.toggleAttribute('_t', this.__titleNodes.length > 0 && !hasHeader);
this.toggleAttribute('_st', this.__subtitleNodes.length > 0 && !hasHeader);
}

/** @private */
Expand Down Expand Up @@ -233,16 +270,6 @@ class Card extends ElementMixin(ThemableMixin(PolylitMixin(LumoInjectionMixin(Li
__getStringTitleElement() {
return this.querySelector('[slot="title"][card-string-title]');
}

/**
* @protected
* @override
*/
createRenderRoot() {
const root = super.createRenderRoot();
root.addEventListener('slotchange', () => this._onSlotChange());
return root;
}
}

defineCustomElement(Card);
Expand Down
10 changes: 10 additions & 0 deletions packages/card/test/dom/__snapshots__/card.test.snap.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ snapshots["vaadin-card host content"] =
`;
/* end snapshot vaadin-card host content */

snapshots["vaadin-card host text content"] =
`<vaadin-card
_c=""
role="region"
>
Text content
</vaadin-card>
`;
/* end snapshot vaadin-card host text content */

snapshots["vaadin-card host footer"] =
`<vaadin-card
_f=""
Expand Down
6 changes: 6 additions & 0 deletions packages/card/test/dom/card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ describe('vaadin-card', () => {
await expect(card).dom.to.equalSnapshot();
});

it('text content', async () => {
card.appendChild(document.createTextNode('Text content'));
await nextUpdate(card);
await expect(card).dom.to.equalSnapshot();
});

it('footer', async () => {
const footer = document.createElement('div');
footer.setAttribute('slot', 'footer');
Expand Down
2 changes: 1 addition & 1 deletion packages/card/test/visual/base/card.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('card', () => {
div.style.padding = '20px';
});

const cardFixture = (content) => fixtureSync(`<vaadin-card>${content}</vaadin-card>`, div);
const cardFixture = (content = '') => fixtureSync(`<vaadin-card>${content}</vaadin-card>`, div);

const mediaFixture = (showImage, theme) => {
const media = showImage
Expand Down
2 changes: 1 addition & 1 deletion packages/card/test/visual/lumo/card.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('card', () => {
div.style.padding = '20px';
});

const cardFixture = (content) => fixtureSync(`<vaadin-card>${content}</vaadin-card>`, div);
const cardFixture = (content = '') => fixtureSync(`<vaadin-card>${content}</vaadin-card>`, div);

describe('slot', () => {
it('content', async () => {
Expand Down
Loading