Add the addClickListener function for Vaadin Cards
Describe your motivation
I would like to have the addClickListener functionality available for the Card component in Vaadin. Many other Vaadin components already support click listeners out of the box, and the lack of this feature on the Card makes it less convenient to use in interactive layouts.
Describe the solution you'd like
I would like the Card component to support addClickListener, similar to how it works in components such as Button, Div, or HorizontalLayout. This would allow developers to easily handle click events directly on a Card without needing additional wrappers or workarounds.
Describe alternatives you've considered
card.getElement().addEventListener("click", event -> selectPackageCard(card, pkg));
Additional context
Adding addClickListener to the Card component would improve consistency across the Vaadin component set and simplify common UI use cases where Cards need to be interactive (e.g., dashboards, selection cards, navigation cards).
Another simple solution:
MyCard extends Card implements ClickNotifier<MyCard>
This is basically the opposite of what I suggest in #7506, to deprecate ClickListener on other layouts. As Card is also essentially just a layout, I’m not in favor of adding a ClickListener support to it.
The use case of a "clickable card" is of course valid, and we want to support that. But the way to implement that while taking accessibility seriously needs more than a simple click listener.
What a "clickable card" essentially means is a convenience for users with a pointer device to easily hit the click target of a control that navigates to a new view or triggers some other action related to the card. The control that triggers the navigation or action is not the card itself, but a link or a button within the card. This link or button is the one that users can focus with the keyboard, with the tab key. And that link or button can either be visible, or visually hidden while still being visible for screen readers.
So to me it seems we need two things:
- A way to extend the click target of a link or a button inside a card to cover the entire card.
- A way to make a link or button (or any component for that matter) “visually hidden” (or "screen-reader-only”).
If both are used together, the card should gain additional focus state styling, to indicate that card is focused when the visually-hidden link/button is technically focused.
Additionally, we might want to make it possible to nest other links/buttons inside a "clickable card", essentially raising them above the main click target using z-index.
If you want to use @knoobie’s workaround, I would suggest adding tabindex="0" and role="button" on the card as well, and adding event listeners for Enter and Space key events.
It depends on how you are using the card but if the card has no actionable item you could wrap the Card with an Anchor.
If the content of the card is too big then you can add an aria-label in the anchor to set a readable name.
If the card has a button inside, I wouldn't recommend a clicklistener (or to wrap the card in an Anchor).
EDIT: This workaround is not really recommended from different accessibility experts so I'm not sure if it's better or worse than a clicklistener
One of the best articles I've read on this is one by Adrian Roselli: https://adrianroselli.com/2020/02/block-links-cards-clickable-regions-etc.html
The TL;DR of it is that the most universal approach is indeed to have a link or button that, through css, covers the desired clickable area (e.g. the entire card).
This approach does have the caveat that the contents of the card become effectively inaccessible to pointer interaction because the link/button covers it, so text cannot be selected, and any interactive elements are unreachable by pointer. The solution is to place such elements in an area of the card not covered by the button, such as the footer.
So, off the top of my head (without having thought about this for more than five minutes), I could imagine a solution like this:
- The Card component has a built-in link and/or button that covers the entire Card area.
- The is hidden/inactivated by default so as not to interfere with the card's contents.
- When enabled, the link/button renders visible hover & focus states (on / around the Card itself)
- The link/button can be enabled somehow, e.g. by providing with a target/clicklistener.
Things I don't know:
- Should it have both a link and a button, so that you can use it for both purposes with correct semantics and with open-in-new-tab support etc?
- Should it by default not cover the footer, in which other interactive elements should be placed? Or do we need an API for configuring that?
Kitty Giraudel suggests that other interactive elements can be bumped "above" the button with z-index, but there's a couple of caveats with this:
- We probably can't do that automagically, so developers would need to handle that manually, and it can be error prone. It also places certain restrictions on where within the Card's structure we can place that link/button.
- As mentioned in the article, those interactive elements would ideally have some inert "safe space" around them, to avoid inadvertent clicks on the link/button behind, which makes the implementation much more complicated.
So I think reserving the footer for interactive elements would be a better approach. It just needs to be clearly documented.
My idea would be to provide a class name (or some other identifier for CSS) that can be applied to a button/link that you place in the card. Then we add styles to the card that adds a pseudo element on such a button/link that covers the card. We can similarly provide a class name that you can apply to any content in the card to promote them above the button/link target area.
I think we could also provide a safe area around them, assuming we could hijack either the ::before or ::after pseudo element on such elements. That should probably be an opt-in thing as well, so that if you know your component already uses those pseudos, you don't want to add the safe area. Update: ah, missed the "inert" part. Yeah, that would probably be very tricky/impossible to implement.
One fairly important note: the button/link that should cover the entire can't use any styles that contain positioned elements inside it, e.g., position: relative or contain: layout, and it should have one pseudo element available for use.
Right, so something like this? https://codepen.io/rsmeds/pen/MYKOdor?editors=1100
I think that makes a lot of sense for when you want to have a visible link or button whose click area is the entire card... and I guess we could have an additional/alternate classname for when you want to hide it visually for when you don't want a visible link/button?
and I guess we could have an additional/alternate classname for when you want to hide it visually for when you don't want a visible link/button?
Yeah, a general purpose .sr-only or .visually-hidden class would suffice.
Hmm, except that the usual implementation of that, with position: absolute, would break the pseudo positioning over the card surface.
But perhaps that can be addressed either by not using absolute positioning, or by applying some additional styles to the the button/link itsel...
Might be an option: https://jsbin.com/edit?html,css,output
.sr-only {
display: block !important;
contain: size !important;
overflow: hidden !important;
outline: 0 !important;
padding: 0 !important;
border: 0 !important;
margin: 0 !important;
width: 0 !important;
height: 0 !important;
}
contain: size collapses the element to 0px size, but doesn't create a position context.