diff --git a/docs/pages/kcard.vue b/docs/pages/kcard.vue
index 0e1dcef4e..b49ff0012 100644
--- a/docs/pages/kcard.vue
+++ b/docs/pages/kcard.vue
@@ -48,7 +48,7 @@
{ text: 'KCard and KCardGrid', href: '#k-card-and-grid' },
{ text: 'Title', href: '#title' },
{ text: 'Accessibility', href: '#a11y' },
- { text: 'Navigation', href: '#navigation' },
+ { text: 'Click event and navigation', href: '#click-navigation' },
{ text: 'Layout', href: '#layout' },
{ text: 'Responsiveness', href: '#responsiveness' },
{ text: 'Content slots', href: '#content-slots' },
@@ -214,28 +214,56 @@
Always test semantics, accessibility, and right-to-left of the final cards.
- Navigation
-
+ Click event and navigation
+
- KCard
's entire area is clickable, navigating to a target provided via the to
prop as a regular Vue route object.
-
-
-
-
-
-
-
-
-
-
-
+ KCard
's entire area is clickable.
+
+ You can use the to
prop to navigate to a URL when the card is clicked.
+
+
+
+
+
+
+
+
+ Listen to the click
event to perform a custom action (whether or not the to
prop is used).
+
+
+
+
+
+
+
+
+
+
+ export default {
+ methods() {
+ onClick() {
+ console.log('Card clicked');
+ }
+ },
+ };
+
+
+
+ Note that long clicks are ignored to allow for text selection.
See to learn how to disable card navigation in favor of a custom handler when elements like buttons are rendered within a card.
@@ -563,7 +591,7 @@
- When adding interactive elements like buttons to a card via slots, apply the .stop
event modifier to their @click
event to prevent the card from navigating away when clicked.
+ When adding interactive elements like buttons to a card via slots, apply the .stop
event modifier to their @click
event to prevent the card .
This applies to all slot content, but considering accessibility is especially important with interactive elements. For instance, ariaLabel
is applied to the bookmark icon button in the following example so that screenreaders can communicate its purpose. In production, more work would be needed to indicate the bookmark's toggled state. Always assess on a case-by-case basis.
diff --git a/lib/cards/KCard.vue b/lib/cards/KCard.vue
index 7a7e93084..340250c8e 100644
--- a/lib/cards/KCard.vue
+++ b/lib/cards/KCard.vue
@@ -36,12 +36,13 @@
selecting card's content in `onClick`)
-->
+
+
+
+
+
+
+
+
+
div > h${headingLevel} > :first-child`);
- expect(fourthElement.element.tagName.toLowerCase()).toBe('a');
expect(fourthElement.exists()).toBe(true);
+ expect(fourthElement.element.tagName.toLowerCase()).toBe('span');
+ expect(fourthElement.element.classList.contains('title')).toBeTruthy();
const linkText = fourthElement.text();
expect(linkText).toBe(title);
@@ -53,16 +54,30 @@ function checkExpectedCardMarkup({ wrapper, headingLevel, title }) {
describe('KCard', () => {
it('renders passed header level', () => {
const wrapper = makeWrapper({
- propsData: { headingLevel: 4, title: 'sample title prop', to: { path: '/some-link' } },
+ propsData: { headingLevel: 4, title: 'sample title prop' },
});
const heading = wrapper.find('h4');
expect(heading.exists()).toBe(true);
});
+ it(`renders the title text as span when 'to' is not provided`, () => {
+ const wrapper = makeWrapper({
+ propsData: { headingLevel: 4, title: 'sample title' },
+ });
+ expect(wrapper.find('.title').element.tagName.toLowerCase()).toBe('span');
+ });
+
+ it(`renders the title text as link when 'to' is provided`, () => {
+ const wrapper = makeWrapper({
+ propsData: { to: { path: '/some-link' }, headingLevel: 4, title: 'sample title' },
+ });
+ expect(wrapper.find('.title').element.tagName.toLowerCase()).toBe('a');
+ });
+
it('renders the correct accessibility structure when title passed via slot', () => {
const wrapper = makeWrapper({
- propsData: { to: { path: '/some-link' }, headingLevel: 4 },
+ propsData: { headingLevel: 4 },
slots: {
title: 'Test Title',
},
@@ -72,38 +87,57 @@ describe('KCard', () => {
it('renders the correct accessibility structure when title passed via prop', () => {
const wrapper = makeWrapper({
- propsData: { to: { path: '/some-link' }, headingLevel: 4, title: 'Test Title' },
+ propsData: { headingLevel: 4, title: 'Test Title' },
});
checkExpectedCardMarkup({ wrapper, headingLevel: 4, title: 'Test Title' });
});
- it('should not navigate on long click to allow for text selection', async () => {
- const wrapper = makeWrapper({
- propsData: { to: { path: '/some-link' }, title: 'sample title prop' },
- });
+ describe('on long click', () => {
+ it('should not emit a click event or navigate to allow for text selection', async () => {
+ const wrapper = makeWrapper({
+ propsData: { to: { path: '/some-link' }, title: 'sample title ' },
+ });
+
+ await wrapper.find('li').trigger('mousedown');
+ await new Promise(resolve => setTimeout(resolve, 500));
+ await wrapper.find('li').trigger('click');
- await wrapper.find('li').trigger('mousedown');
- await new Promise(resolve => setTimeout(resolve, 500));
- await wrapper.find('li').trigger('click');
- expect(wrapper.vm.$router.currentRoute.path).not.toBe('/some-link');
+ expect(wrapper.emitted().click).toBeFalsy();
+ expect(wrapper.vm.$router.currentRoute.path).not.toBe('/some-link');
+ });
});
- it('should navigate on quick click', async () => {
- const wrapper = makeWrapper({
- propsData: { to: { path: '/some-link' }, title: 'sample title ' },
+ describe('on short click', () => {
+ it('should emit a click event', async () => {
+ const wrapper = makeWrapper({
+ propsData: { title: 'sample title ' },
+ });
+
+ await wrapper.find('li').trigger('mousedown');
+ await new Promise(resolve => setTimeout(resolve, 100));
+ await wrapper.find('li').trigger('click');
+
+ expect(wrapper.emitted().click).toBeTruthy();
});
- await wrapper.find('li').trigger('mousedown');
- await new Promise(resolve => setTimeout(resolve, 100));
- await wrapper.find('li').trigger('click');
+ it(`when 'to' provided, should both navigate and emit a click event`, async () => {
+ const wrapper = makeWrapper({
+ propsData: { to: { path: '/some-link' }, title: 'sample title ' },
+ });
+
+ await wrapper.find('li').trigger('mousedown');
+ await new Promise(resolve => setTimeout(resolve, 100));
+ await wrapper.find('li').trigger('click');
- expect(wrapper.vm.$router.currentRoute.path).toBe('/some-link');
+ expect(wrapper.emitted().click).toBeTruthy();
+ expect(wrapper.vm.$router.currentRoute.path).toBe('/some-link');
+ });
});
describe('it renders slotted content', () => {
it('renders slotted content with aboveTitle slot', () => {
const wrapper = makeWrapper({
- propsData: { to: { path: '/some-link' }, title: 'sample title ' },
+ propsData: { title: 'sample title ' },
slots: {
aboveTitle: 'above title',
},
@@ -116,7 +150,7 @@ describe('KCard', () => {
it('renders slotted content with belowTitle slot', () => {
const wrapper = makeWrapper({
- propsData: { to: { path: '/some-link' }, title: 'sample title ' },
+ propsData: { title: 'sample title ' },
slots: {
belowTitle: 'below title',
},
@@ -129,7 +163,7 @@ describe('KCard', () => {
it('renders slotted content with footer slot', () => {
const wrapper = makeWrapper({
- propsData: { to: { path: '/some-link' }, title: 'sample title ' },
+ propsData: { title: 'sample title ' },
slots: {
footer: 'footer slot content goes here',
},
@@ -144,7 +178,6 @@ describe('KCard', () => {
it('preserves space when preserve prop is true and slot is not empty', () => {
const wrapper = makeWrapper({
propsData: {
- to: { path: '/some-link' },
title: 'sample title ',
headingLevel: 4,
preserveAboveTitle: true,
@@ -168,7 +201,6 @@ describe('KCard', () => {
it('preserves space when preserve prop is true and slot is empty', () => {
const wrapper = makeWrapper({
propsData: {
- to: { path: '/some-link' },
title: 'sample title ',
headingLevel: 4,
preserveAboveTitle: true,
@@ -188,7 +220,6 @@ describe('KCard', () => {
it('removes space when preserve prop is false and slot is empty', () => {
const wrapper = makeWrapper({
propsData: {
- to: { path: '/some-link' },
title: 'sample title ',
headingLevel: 4,
preserveAboveTitle: false,
@@ -208,7 +239,6 @@ describe('KCard', () => {
it('show slots content regardless of whether the preserve prop is true', () => {
const wrapper = makeWrapper({
propsData: {
- to: { path: '/some-link' },
title: 'sample title ',
headingLevel: 4,
preserveAboveTitle: true,
@@ -234,7 +264,6 @@ describe('KCard', () => {
it('is not displayed for thumbnail display none', () => {
const wrapper = makeWrapper({
propsData: {
- to: { path: '/some-link' },
title: 'sample title ',
headingLevel: 4,
thumbnailDisplay: 'none',
@@ -249,7 +278,6 @@ describe('KCard', () => {
it('is displayed when a thumbnail image source is not provided', () => {
const wrapper = makeWrapper({
propsData: {
- to: { path: '/some-link' },
title: 'sample title ',
headingLevel: 4,
thumbnailSrc: null,
@@ -266,7 +294,6 @@ describe('KCard', () => {
it('is displayed when a thumbnail image could not be loaded', async () => {
const wrapper = makeWrapper({
propsData: {
- to: { path: '/some-link' },
title: 'sample title ',
headingLevel: 4,
thumbnailSrc: '/thumbnail-img.jpg',
@@ -287,7 +314,6 @@ describe('KCard', () => {
it('is not displayed when a thumbnail image is available', () => {
const wrapper = makeWrapper({
propsData: {
- to: { path: '/some-link' },
title: 'sample title ',
headingLevel: 4,
thumbnailSrc: '/thumbnail-img.jpg',