@@ -152,9 +152,6 @@
if (this.title) {
return this.title;
}
- if (this.$slots.title) {
- return null;
- }
return `${this.prependTitle} Learn everything about hummingbirds: their habitats, feeding patterns, and stunning flight abilities`;
},
diff --git a/docs/pages/kcard.vue b/docs/pages/kcard.vue
index b49ff0012..e92fdfeca 100644
--- a/docs/pages/kcard.vue
+++ b/docs/pages/kcard.vue
@@ -17,7 +17,7 @@
:orientation="windowBreakpoint > 2 ? 'horizontal' : 'vertical'"
thumbnailDisplay="large"
thumbnailAlign="right"
- prependTitle="(2)"
+ prependTitle="(1)"
showProgressInFooter
/>
@@ -33,6 +33,7 @@
Set a correct heading level ()
Ensure each card title is unique within a card grid ()
+ Do not use a heading element within the title
slot
Ensure content provided via slots is accessible ()
Even if a thumbnail image is available, provide a placeholder element ()
If using selection controls, use pre-defined labels ()
@@ -148,7 +149,7 @@
- Use the title
prop to assign an unique title to each card in a grid, and the headingLevel
prop to set the heading level on it. The level needs to correspond to the surrounding context.
+ Always use the title
prop to assign an unique title to each card in a grid, and the headingLevel
prop to set the heading level on it. The level needs to correspond to the surrounding context.
Examples:
@@ -159,40 +160,79 @@
- The titleMaxLines
prop can be used to truncate the title to a set number of lines.
+ The scoped title
slot with its titleText
attribute can be used to customize the title.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- For more customization, the title
slot can be used. Provide only a title text to the slot without wrapping it in a heading element to avoid duplicate headings in the markup output.
+ Do not use a heading element within the title
slot to avoid duplicate headings in the markup output.KCard
already handles a heading element internally.
-
-
-
-
-
-
-
- Card title
-
-
-
-
-
-
-
-
- Card title
+
+
+
+
+
+
@@ -202,6 +242,8 @@
+ The titleMaxLines
prop can be used to truncate the title to a set number of lines.
+
Accessibility
@@ -889,6 +931,15 @@
height: '180px',
},
],
+ skeletonsConfig9: [
+ {
+ breakpoints: [0, 1, 2, 3, 4, 5, 6, 7],
+ orientation: 'horizontal',
+ thumbnailDisplay: 'small',
+ thumbnailAlign: 'right',
+ height: '130px',
+ },
+ ],
};
},
mounted() {
diff --git a/lib/cards/KCard.vue b/lib/cards/KCard.vue
index 84319c6c7..fee4137ec 100644
--- a/lib/cards/KCard.vue
+++ b/lib/cards/KCard.vue
@@ -17,9 +17,19 @@
:class="['card-area', ...cardAreaClasses ]"
:style="{ backgroundColor: $themeTokens.surface }"
>
+
+
+ {{ title }}
+
@@ -41,46 +51,46 @@
:to="to"
draggable="false"
class="title"
+ :aria-labelledby="`card-title-${_uid}`"
@focus.native="onTitleFocus"
@blur.native="onTitleBlur"
>
-
-
-
+
+
+
+
+
-
-
+
-
-
- all other content
-
-
- "li > h[2-6] > a > title text" structure that comes before
- all other content is crucial to ensure accessibility.
-*/
-function checkExpectedCardMarkup({ wrapper, headingLevel, title }) {
- const firstElement = wrapper.find(':first-child');
- expect(firstElement.exists()).toBe(true);
- expect(firstElement.element.tagName.toLowerCase()).toBe('li');
-
- const secondElement = wrapper.find('li > :first-child');
- expect(secondElement.exists()).toBe(true);
- expect(secondElement.element.tagName.toLowerCase()).toBe('div');
-
- const thirdElement = wrapper.find('li > div > :first-child');
- expect(thirdElement.exists()).toBe(true);
- expect(thirdElement.element.tagName.toLowerCase()).toBe(`h${headingLevel}`);
-
- const fourthElement = wrapper.find(`li > div > h${headingLevel} > :first-child`);
- 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);
-}
-
describe('KCard', () => {
it('renders passed header level', () => {
const wrapper = makeWrapper({
@@ -61,35 +23,93 @@ describe('KCard', () => {
expect(heading.exists()).toBe(true);
});
- it(`renders the title text as span when 'to' is not provided`, () => {
+ it(`renders the correct accessibility structure when card is link`, () => {
const wrapper = makeWrapper({
- propsData: { headingLevel: 4, title: 'sample title' },
+ propsData: { to: { path: '/some-link' }, title: 'Test Title', headingLevel: 4 },
});
- 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');
+ // Check for:
+ //
+ // li
+ // |-- div
+ // |--span (visually hidden title text)
+ // |-- h[2-6]
+ // |--a (with aria-labelledby pointing to the span above)
+ // |-- span (with aria-hidden)
+ //
+ const el1 = wrapper.find(':first-child');
+ expect(el1.exists()).toBe(true);
+ expect(el1.element.tagName.toLowerCase()).toBe('li');
+
+ const el2 = wrapper.find('li > :first-child');
+ expect(el2.exists()).toBe(true);
+ expect(el2.element.tagName.toLowerCase()).toBe('div');
+
+ const el3 = wrapper.find('li > div > :nth-child(1)');
+ expect(el3.exists()).toBe(true);
+ expect(el3.element.tagName.toLowerCase()).toBe('span');
+ expect(el3.classes()).toContain('visuallyhidden');
+ expect(el3.text()).toBe('Test Title');
+ const spanId = el3.attributes('id');
+
+ const el4 = wrapper.find('li > div > :nth-child(2)');
+ expect(el4.exists()).toBe(true);
+ expect(el4.element.tagName.toLowerCase()).toBe('h4');
+
+ const el5 = wrapper.find(`li > div > h4 > :first-child`);
+ expect(el5.exists()).toBe(true);
+ expect(el5.element.tagName.toLowerCase()).toBe('a');
+ expect(el5.attributes('aria-labelledby')).toBe(spanId);
+
+ const el6 = wrapper.find(`li > div > h4 > a > :first-child`);
+ expect(el6.exists()).toBe(true);
+ expect(el6.element.tagName.toLowerCase()).toBe('span');
+ expect(el6.attributes('aria-hidden')).toBe('true');
});
- it('renders the correct accessibility structure when title passed via slot', () => {
+ it(`renders the correct accessibility structure when card is not link`, () => {
const wrapper = makeWrapper({
- propsData: { headingLevel: 4 },
- slots: {
- title: 'Test Title',
- },
+ propsData: { title: 'Test Title', headingLevel: 4 },
});
- checkExpectedCardMarkup({ wrapper, headingLevel: 4, title: 'Test Title' });
- });
- it('renders the correct accessibility structure when title passed via prop', () => {
- const wrapper = makeWrapper({
- propsData: { headingLevel: 4, title: 'Test Title' },
- });
- checkExpectedCardMarkup({ wrapper, headingLevel: 4, title: 'Test Title' });
+ // Check for:
+ //
+ // li
+ // |-- div
+ // |--span (visually hidden title text)
+ // |-- h[2-6]
+ // |--span (focusable and with aria-labelledby pointing to the span above)
+ // |-- span (with aria-hidden)
+ //
+ const el1 = wrapper.find(':first-child');
+ expect(el1.exists()).toBe(true);
+ expect(el1.element.tagName.toLowerCase()).toBe('li');
+
+ const el2 = wrapper.find('li > :first-child');
+ expect(el2.exists()).toBe(true);
+ expect(el2.element.tagName.toLowerCase()).toBe('div');
+
+ const el3 = wrapper.find('li > div > :nth-child(1)');
+ expect(el3.exists()).toBe(true);
+ expect(el3.element.tagName.toLowerCase()).toBe('span');
+ expect(el3.classes()).toContain('visuallyhidden');
+ expect(el3.text()).toBe('Test Title');
+ const spanId = el3.attributes('id');
+
+ const el4 = wrapper.find('li > div > :nth-child(2)');
+ expect(el4.exists()).toBe(true);
+ expect(el4.element.tagName.toLowerCase()).toBe('h4');
+
+ const el5 = wrapper.find(`li > div > h4 > :first-child`);
+ expect(el5.exists()).toBe(true);
+ expect(el5.element.tagName.toLowerCase()).toBe('span');
+ expect(el5.attributes('aria-labelledby')).toBe(spanId);
+ expect(el5.attributes('tabindex')).toBe('0');
+
+ const el6 = wrapper.find(`li > div > h4 > span > :first-child`);
+ expect(el6.exists()).toBe(true);
+ expect(el6.element.tagName.toLowerCase()).toBe('span');
+ expect(el6.attributes('aria-hidden')).toBe('true');
});
describe('on long click', () => {