Skip to content
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

[feat] Card slots #8233

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open

[feat] Card slots #8233

wants to merge 30 commits into from

Conversation

jouni
Copy link
Member

@jouni jouni commented Nov 27, 2024

This prototype introduces six slots to the Card component: media, title, subtitle, title-suffix, content, and suffix. The layout can be vertical (the default) or horizontal.

The media slot is meant for images, icons, or avatars, but you can put anything there if you want to. The media component/element layout can be controlled slightly with the cover-media and stretch-media theme variants, which make it cover or stretch the full width/height of the card.

Screenshot 2024-11-27 at 12 34 30

The implementation has some quirks/limitations, especially the horizontal variant:

  • Horizontal: if you add a media element, and a content element (i.e., :not([slot])), but no title or subtitle, the content element will be below the media, instead of next to it. Similarly with the suffix element.
  • Horizontal: the "cover-media" and "stretch-media" variants don't work nicely with icon or avatar.
  • Vertical & horizontal: if you don't add a subtitle, the title-suffix element will still span two rows, creating an unnecessary CSS grid row and an extra gap in the vertical variant.

@jouni
Copy link
Member Author

jouni commented Nov 27, 2024

I’m still going to try and implement this without using CSS grid gaps, relying on margins on the content, and using named grid areas to place the content.

Note, that the implementation would be much easier if Chrome didn't have a bug and it supported the :host(:has()) selector combination (which works in Safari and Firefox). That would allow us to define a bespoke CSS grid for every content configuration, instead of relying on auto-placement (like this first prototype) or working around unused grid rows and columns.

@jouni jouni marked this pull request as draft November 27, 2024 10:50
@jouni
Copy link
Member Author

jouni commented Nov 27, 2024

I’m now asking, if this complexity is warranted, or should we limit the slots and/or name them differently.

A more simplified version would be, for example, to have prefix, title, content, and suffix slots. If you need a subtitle, you add that in the title slot, after your main title. This means that the styling of the content would be left for the developer vs in this first prototype where some content is styled automatically.

@jouni
Copy link
Member Author

jouni commented Nov 27, 2024

@TatuLund and @knoobie were the first ones to ask for something like after the first Card component implementation (with just one slot), so I’m eager to hear your thoughts.

@jouni jouni changed the title vertical orientation works alright, horizontal has issues [feat] Card slots Nov 27, 2024
@knoobie
Copy link
Contributor

knoobie commented Nov 27, 2024

It's not what I expected to be honest.. but your vision looks more like a card in a card sense 🙂 I'm not sure if this creates more problems than it would solve with the amount of slots.. like three slots for the title (including subtitle and badge).. this feels kinda overenginered and hard to configure correctly or adjust to e.g. project needs for example the title has to be above the image or the badge before the title. I would personally go with something like this:

  • slot=prefix (header?)
  • slot=media
  • slot=title
  • slot/content
  • slot=suffix (footer?)

Prefix slot could be the exchange for the title slot for people or could be used to add a close button that with some css flows over the image

Did people really want a horizontal card? I've rarely seen some.. but when I see them it's like image left / everything else on the right.. probably something like this:

  • prefix spawning all the way
  • Media 50% / title 50% and below title content
  • suffix spawning all the way

This would probably need a little bit more DOM structure or some CSS voodoo

@TatuLund
Copy link
Contributor

I think this approach is much better. Card is conceptually a "simple" component, but there are still number of choices involved. So this will be opinion based. But that is exactly the point. When you are not a designer, and just want a component where those opinion based design choices has been made for you and you have some API for couple of alternatives. Like here for example media content is naturally optional, and it can stretched if you want. I would aim for a pareto here. I.e. have features and options to cover 70-80% typical scenarios. In the use cases where this is not enough, it is better to write a custom component for the specific scenario.

@jouni
Copy link
Member Author

jouni commented Nov 29, 2024

I see the first iteration and this prototype at the opposite ends of the “complexity-opinionation-spectrum”. The first iteration is very simple and has very little opinion about what you do with the card. This prototype is complex, and has plenty of opinion regarding the content.

Now we’re supposedly looking for the sweet spot somewhere in the middle, in addition to agreeing what are the opinions we want to bake in by default.

As an example, which one do you prefer:

<vaadin-card>
  <div class="font-l text-header semibold leading-xs">Title</div>
  <div class="text-secondary leading-xs">Subtitle</div>
</div>
<vaadin-card>
  <div slot="title">Title</div>
  <div slot="subttitle">Subtitle</div>
</div>

Or what could some middle ground between them look like?

@jouni
Copy link
Member Author

jouni commented Nov 29, 2024

project needs for example the title has to be above the image

Could you consider adding the image in the content slot in this case?

@jouni
Copy link
Member Author

jouni commented Nov 29, 2024

Did people really want a horizontal card? I've rarely seen some.

Here’s where I picked up the horizontal variant use case: https://m3.material.io/components/cards/guidelines
Screenshot 2024-11-29 at 16 33 18

but when I see them it's like image left / everything else on the right

Yeah, I’m arriving at the same conclusion, which relates to

slot=suffix (footer?)

that as well. I started with a “footer” slot, but changed that hastily to suffix when I worked on the horizontal variant, assuming you’d want any possible action buttons at the end of the card. But you likely still want them at the bottom of the content/card, and there’s likely another slot for the suffix content, like you suggested.

Screenshot 2024-11-29 at 16 37 50

@jouni
Copy link
Member Author

jouni commented Nov 29, 2024

Somewhere along the spectrum is something like this:

<vaadin-card>
  <img slot="media">

  <vaadin-card-header>
    <div slot="title">Title</div>
    <div slot="subtitle">Subtitle</div>
  </vaadin-card-header>

  <vaadin-card-content>
    ...
  </vaadin-card-content>

  <vaadin-card-footer>
    <button>Action</button>
  </vaadin-card-footer>
</vaadin-card>

@knoobie
Copy link
Contributor

knoobie commented Nov 29, 2024

As an example, which one do you prefer

Definitely the second one with less styling (which of course is the opposite of my arguments from above). I think the thing that is mostly off-putting myself is the suffix slot of the title. Don't get me wrong.. we also use that position a lot.. it probably comes from the fact that the prefix slot is missing compared to other components where (normally) both kinds are available. (Now I'm also making it more complex.. even tho some could argue it's less complex CSS (hopefully))

OT: do you see a card as section or article from an accessibility standpoint? And do you also have opinion on the hX-lvl of the title and subtitle or do you let this completely free for people to do with the slotted content? Which of course also means aria-labelledBy association with the card.

Could you consider adding the image in the content slot in this case?

Of course 🙂 people always find solutions.. they just have to understand that your nice fitting/scaling is not available out of the box which they get from your media slot

Here’s where I picked up the horizontal variant use case

Now I understand where your vaadin-item thought comes from.. because that first one really looks like an selectable item and not like a vaadin-card to me. But using different components based on available space is probably a little bit overkill.

Somewhere along the spectrum is something like this

From a semantics standpoint this looks kinda better.. but I've got the feeling it could confuse people more than it helps or I might be totally wrong because I'm not your typical react / typescript user.. slots might come with the benefit that you could theoretically also do something like this <vaadin-card title="Lorem" media="url" content="Blub"></vaadin-card> for the really simple use cases

@jouni
Copy link
Member Author

jouni commented Dec 3, 2024

do you see a card as section or article from an accessibility standpoint?

I’m not sure. I was thinking of leaving that decision for the developer. Would you expect a default role?

And do you also have opinion on the hX-lvl of the title and subtitle or do you let this completely free for people to do with the slotted content? Which of course also means aria-labelledBy association with the card.

The plan is that you choose what you slot in, the component doesn't have an opinion. You can slot in a heading element, but they you might need to tweak the styles yourself, as Lumo has default styling for heading elements, and the Card component's shadow DOM styles can't override those.

If you slot in a heading element, perhaps we should pick that up and connect it with the card container with aria-labelledby automatically?

@jouni
Copy link
Member Author

jouni commented Dec 3, 2024

Some more riffing on the use cases and API.

The basic case, including the horizontal variant:

<vaadin-card> <!--- optionally theme="horizontal" -->
  <img slot="media" src="...">
  <div slot="title">Performances at The Hideout</div>
  <div>Watch exclusive performances...</div>
  <button slot="footer" class="self-end">Get tickets</button>
</vaadin-card>

A more complex header (prefix and suffix elements):

Screenshot 2024-12-03 at 8 40 55
<vaadin-card>
  <vaadin-avatar slot="header-prefix" src="..."></vaadin-avatar>
  <div slot="title" class="font-s">Daniel Maas</div>
  <div slot="subtitle" class="font-s">Yesterday</div>
  <vaadin-button slot="header-suffix" theme="icon"><vaadin-icon icon="vaadin:star-o"></vaadin-icon></vaadin-button>
  <div class="font-l text-header">Clay pot fair on Saturday?</div>
  <p>I think it’s time for us to...</p>
</vaadin-card>

Multiple actions areas; header before media (could possibly be a built-in variant instead of custom CSS):

Screenshot 2024-12-03 at 8 42 40
<style>
  vaadin-card::part(header) { order: -1; }
</style>
<vaadin-card>
  <vaadin-avatar slot="header-prefix" src="..."></vaadin-avatar>
  <div slot="header-suffix">
    <vaadin-button theme="small">
      <vaadin-icon icon="vaadin:heart-o" slot="prefix"></vaadin-icon>
      <span>Preferido</span>
    </vaadin-button>
    <vaadin-button theme="small">
      <vaadin-icon icon="vaadin:calendar" slot="prefix"></vaadin-icon>
      <span>Ayudar</span>
    </vaadin-button>
  </div>
  <img slot="media" src="...">
  <div>Caminante, son tus huellas el camino...</div>
  <div slot="footer" class="flex justify-between">
    <div>
      <vaadin-button>Escucha</vaadin-button>
      <vaadin-button>Ahorrar</vaadin-button>
    </div>
    <div>
      <vaadin-button theme="tertiary icon">
        <vaadin-icon icon="vaadin:phone"></vaadin-icon>
      </vaadin-button>
      <vaadin-button theme="tertiary icon">
        <vaadin-icon icon="vaadin:chat"></vaadin-icon>
      </vaadin-button>
    </div>
  </div>
</vaadin-card>

Another canonical card example:

Screenshot 2024-12-03 at 8 45 33
<style>
  vaadin-card::part(header) { order: -1; }
</style>
<vaadin-card>
  <vaadin-avatar slot="header-prefix" abbr="R"></vaadin-avatar>
  <div slot="title" class="font-m">Shrimp and Chorizo Paella</div>
  <div slot="subtitle" class="font-m">September 14, 2016</div>
  <vaadin-button slot="header-suffix" theme="tertiary icon"><vaadin-icon icon="vaadin:ellipsis-v"></vaadin-icon></vaadin-button>
  <p>This impressive paella is...</p>
  <div slot="footer" class="flex justify-between">
    <div>
      <vaadin-button theme="tertiary icon">
        <vaadin-icon icon="vaadin:heart"></vaadin-icon>
      </vaadin-button>
      <vaadin-button theme="tertiary icon">
        <vaadin-icon icon="vaadin:share"></vaadin-icon>
      </vaadin-button>
    </div>
    <div>
      <vaadin-button theme="tertiary icon">
        <vaadin-icon icon="lumo:chevron-down"></vaadin-icon>
      </vaadin-button>
    </div>
  </div>
</vaadin-card>

The last two examples make make me consider adding one more slot, footer-end.

@rolfsmeds
Copy link
Contributor

rolfsmeds commented Dec 3, 2024

project needs for example the title has to be above the image

That sounds like a Card style variant to me, that re-arranges the slots visually using css grid. (IIRC we do the same in other components, and from an a11y POV it should not be problematic since we're talking about an image).

I think the thing that is mostly off-putting myself is the suffix slot of the title.

Although you can just not use slots you don't want to use, I do think we could well have just a single slot between image and content, and an API that allows you to just pass a string but also supports placing elements there if you need a subtitle and/or a badge or something on the side.

@@ -54,7 +54,7 @@ const card = css`
var(--vaadin-card-box-shadow);
}

:host(:where([theme~='cover-media'])) ::slotted([slot='media']) {
:host(:where([theme~='cover-media'])) ::slotted([slot='media']:is(img, video, svg, vaadin-icon)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also contain picture?

@knoobie
Copy link
Contributor

knoobie commented Dec 4, 2024

Would you expect a default role?

Personally I would go with section but only if people have supplied either a title text or component so that it can be associated together.. having a section without headline does not help

If you slot in a heading element, perhaps we should pick that up and connect it with the card container with aria-labelledby automatically?

Definitly! See above 🙂

visually using css grid

Is it okay to use? I remember nasty accessibility problems when e.g. using visual ordering vs DOM ordering with tab key and such making problems again..


Good job with the new DOM structure! Looks really promising

@TatuLund
Copy link
Contributor

TatuLund commented Dec 5, 2024

Good job with the new DOM structure! Looks really promising

Yes, I agree. I have had a TODO idea in my mind to do it very similar fashion. There are couple of better CSS ideas here than in the model that I had in my head.

@jouni
Copy link
Member Author

jouni commented Dec 5, 2024

I remember nasty accessibility problems when e.g. using visual ordering vs DOM ordering with tab key and such making problems again..

I think it would be a problem, if you place something else than visuals/decoration in the media slot. But if it’s an image/icon, it shouldn't matter, if it’s not announced at all. But yeah, it’s something to point out in documentation if we do something like that (a variant that changes the visual order).

@jouni
Copy link
Member Author

jouni commented Dec 5, 2024

I recorded a short video going through the current prototype, hoping it’ll give a good overview of the features at the moment, making it easier to give feedback. Apologies for the poor quality/low resolution – GitHub has a 100MB limitation on attachments.

card-prototype-demo.mov

@knoobie
Copy link
Contributor

knoobie commented Dec 5, 2024

Looks really good! Only thing I would probably ask for: add some CSS for the horizontal + low resolution that the card back to normal.. otherwise it might look really ugly / unfinished.

Btw: you might wanna share the video in the forum.. i think I saw multiple people in the past requesting this component.. might be really good to get their feedback as well

@jouni
Copy link
Member Author

jouni commented Dec 5, 2024

add some CSS for the horizontal + low resolution that the card back to normal.. otherwise it might look really ugly / unfinished.

Sorry, I didn't quite understand what you meant.

you might wanna share the video in the forum

Yeah, thanks for the suggestion. I’ll do that.

@knoobie
Copy link
Contributor

knoobie commented Dec 5, 2024

Ups.. mobile phone's autocorrection removed some words.. At 4:12 min in your video you show case how horizontal cards behave when shrinking... I would suggest: once the available space for the image is less then 200px it goes back to be a non-horizontal card and the image goes back to the top.. something similar could probably be done with prefix/suffix of the title which looks kinda out-of-place at 2:02 min

@jouni
Copy link
Member Author

jouni commented Dec 5, 2024

Got it. Yeah, responsive layouting did cross my mind at some point. That could be an additonal theme variant perhaps. When to change the orientation, I haven't thought too much about that yet.

@jouni
Copy link
Member Author

jouni commented Dec 10, 2024

First draft of the documentation: https://docs.google.com/document/d/1ZWJNgvgGyPAM-KdctcbOC8vBitgQvWUDnLFc7ugBBQg/edit?usp=sharing

Now I should either work on refactoring the tests, or figuring out the Flow and React APIs.

@jouni jouni marked this pull request as ready for review December 18, 2024 12:46
Comment on lines +106 to +113
:host(:is(:has(> :not([slot])), [has-content])) {
--_content: 1;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this doesn't support plain text as card content, e.g. the following works:

<vaadin-card>
  <div>Content</div>
</vaadin-card>

But the following does not work (the content isn't visible):

<vaadin-card>Content</vaadin-card>

);
this.toggleAttribute('has-header-prefix', this.querySelector(':scope > [slot="header-prefix"]'));
this.toggleAttribute('has-header-suffix', this.querySelector(':scope > [slot="header-suffix"]'));
this.toggleAttribute('has-content', this.querySelector(':scope > :not([slot])'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be probably updated to check for the actual slotted nodes as querySelector() doesn't return text nodes from the default slot. I could change the logic to use SlotObserver helper that we have since it provides the access to the list of current nodes using slot.assignedNodes({ flatten: true }).


/** @private */
_onSlotChange() {
// Chrome doesn't support `:host(:has())`, so we'll recreate that with custom attributes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we want to provide has- attributes for all browsers? This approach is widely used by existing components e.g. has-error-message etc and IMO it would make card styling easier.

Copy link
Member

@web-padawan web-padawan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some tweaks to visual tests to improve readability in #8369 and merged it to this PR.

@web-padawan
Copy link
Member

Rebased and updated screenshots again since main branch now uses Chrome 130 for both Lumo and Material visual tests.

Copy link
Member

@web-padawan web-padawan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reported #8479 and #8480 for the issues mentioned above that are still unresolved. Otherwise LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants