Skip to content

Commit ff8f6d3

Browse files
AdminXVIIquinnlangille
authored andcommitted
feat(accessibility): use correct aria properties (#349)
* chore: build * feat(accessibility): use correct aria properties Based on [aXe](https://www.deque.com/axe/) recommendations, fix accessibility: - Use proper roles for the various components (ex: role="button" not needed on buttons). - Reorder navigation and pagination to better match the tabbing order. - Make navigation accessible by tabbing. - Change the HTML elements to better match [W3's recommended organization](https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html)
1 parent a895b28 commit ff8f6d3

File tree

9 files changed

+208
-276
lines changed

9 files changed

+208
-276
lines changed

dist/vue-carousel.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/themes/vue/source/js/vue-carousel.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Carousel.vue

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
'VueCarousel-inner',
1414
{ 'VueCarousel-inner--center': isCenterModeEnabled }
1515
]"
16-
role="listbox"
1716
:style="{
1817
'transform': `translate(${currentOffset}px, 0)`,
1918
'transition': dragging ? 'none' : transitionStyle,
@@ -30,10 +29,6 @@
3029
</div>
3130
</div>
3231

33-
<slot name="pagination" v-if="paginationEnabled">
34-
<pagination @paginationclick="goToPage($event, 'pagination')"/>
35-
</slot>
36-
3732
<slot name="navigation" v-if="navigationEnabled">
3833
<navigation
3934
v-if="isNavigationRequired"
@@ -43,6 +38,10 @@
4338
@navigationclick="handleNavigation"
4439
/>
4540
</slot>
41+
42+
<slot name="pagination" v-if="paginationEnabled">
43+
<pagination @paginationclick="goToPage($event, 'pagination')"/>
44+
</slot>
4645
</section>
4746
</template>
4847
<script>

src/Navigation.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<button
44
type="button"
55
aria-label="Previous page"
6-
role="button"
6+
:tabindex="canAdvanceBackward ? 0 : -1"
77
class="VueCarousel-navigation-button VueCarousel-navigation-prev"
88
v-on:click.prevent="triggerPageAdvance('backward')"
99
v-bind:class="{ 'VueCarousel-navigation--disabled': !canAdvanceBackward }"
@@ -12,7 +12,7 @@
1212
<button
1313
type="button"
1414
aria-label="Next page"
15-
role="button"
15+
:tabindex="canAdvanceForward ? 0 : -1"
1616
class="VueCarousel-navigation-button VueCarousel-navigation-next"
1717
v-on:click.prevent="triggerPageAdvance()"
1818
v-bind:class="{ 'VueCarousel-navigation--disabled': !canAdvanceForward }"
@@ -94,6 +94,10 @@ export default {
9494
outline: none;
9595
}
9696
97+
.VueCarousel-navigation-button:focus {
98+
outline: 1px solid lightblue;
99+
}
100+
97101
.VueCarousel-navigation-next {
98102
right: 0;
99103
transform: translateY(-50%) translateX(100%);

src/Pagination.vue

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,28 @@
44
class="VueCarousel-pagination"
55
v-bind:class="{ [`VueCarousel-pagination--${paginationPositionModifierName}`]: paginationPositionModifierName }"
66
>
7-
<ul class="VueCarousel-dot-container" role="tablist">
8-
<li
7+
<div class="VueCarousel-dot-container" role="tablist" :style="`margin-top: ${carousel.paginationPadding * 2}px;`">
8+
<button
9+
v-for="(page, index) in paginationCount"
10+
:key="`${page}_${index}`"
911
class="VueCarousel-dot"
1012
aria-hidden="false"
11-
role="presentation"
13+
role="tab"
14+
:title="`Item ${index}`"
15+
:value="`Item ${index}`"
16+
:aria-label="`Item ${index}`"
1217
:aria-selected="isCurrentDot(index) ? 'true' : 'false'"
1318
v-bind:class="{ 'VueCarousel-dot--active': isCurrentDot(index) }"
14-
v-for="(page, index) in paginationCount"
15-
:key="`${page}_${index}`"
1619
v-on:click="goToPage(index)"
1720
:style="`
1821
margin-${paginationPropertyBasedOnPosition}: ${carousel.paginationPadding * 2}px;
1922
padding: ${carousel.paginationPadding}px;
23+
width: ${carousel.paginationSize}px;
24+
height: ${carousel.paginationSize}px;
25+
background-color: ${isCurrentDot(index) ? carousel.paginationActiveColor : carousel.paginationColor};
2026
`"
21-
>
22-
<button
23-
type="button"
24-
role="button"
25-
aria-label="`Item ${index}`"
26-
:title="`Item ${index}`"
27-
class="VueCarousel-dot-button"
28-
:tabindex="0"
29-
:style="`
30-
width: ${carousel.paginationSize}px;
31-
height: ${carousel.paginationSize}px;
32-
background: ${isCurrentDot(index) ? carousel.paginationActiveColor : carousel.paginationColor};
33-
`"
34-
></button>
35-
</li>
36-
</ul>
27+
></button>
28+
</div>
3729
</div>
3830
</template>
3931

@@ -112,19 +104,16 @@ export default {
112104
.VueCarousel-dot {
113105
display: inline-block;
114106
cursor: pointer;
115-
}
116-
117-
.VueCarousel-dot-button {
118107
appearance: none;
119108
border: none;
120-
background-color: transparent;
109+
background-clip: content-box;
110+
box-sizing: content-box;
121111
padding: 0;
122112
border-radius: 100%;
123113
outline: none;
124-
cursor: pointer;
125114
}
126115
127-
.VueCarousel-dot-button:focus {
116+
.VueCarousel-dot:focus {
128117
outline: 1px solid lightblue;
129118
}
130119
</style>

src/Slide.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<div
33
class="VueCarousel-slide"
44
tabindex="-1"
5+
:aria-hidden="!isActive"
6+
role="tabpanel"
57
:class="{
68
'VueCarousel-slide-active': isActive,
79
'VueCarousel-slide-center': isCenter,

tests/client/components/__snapshots__/carousel.spec.js.snap

Lines changed: 121 additions & 171 deletions
Large diffs are not rendered by default.

tests/client/components/__snapshots__/navigation.spec.js.snap

Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,100 +2,90 @@
22

33
exports[`Navigation should mount successfully 1`] = `
44
".VueCarousel-wrapper
5-
.VueCarousel-inner(role='listbox', style='transform: translate(0px, 0); visibility: hidden; height: auto;')
6-
.VueCarousel-slide(tabindex='-1')
7-
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1')
8-
|
9-
.VueCarousel-pagination(style='')
10-
ul.VueCarousel-dot-container(role='tablist')
11-
li.VueCarousel-dot(aria-hidden='false', role='presentation', aria-selected='false', style='margin-top: 20px; padding: 10px;')
12-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 0', tabindex='0', style='width: 10px; height: 10px; background: rgb(239, 239, 239);')
13-
li.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='presentation', aria-selected='true', style='margin-top: 20px; padding: 10px;')
14-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 1', tabindex='0', style='width: 10px; height: 10px; background: rgb(0, 0, 0);')
5+
.VueCarousel-inner(style='transform: translate(0px, 0); visibility: hidden; height: auto;')
6+
.VueCarousel-slide(tabindex='-1', role='tabpanel', aria-hidden='true')
7+
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1', role='tabpanel')
158
|
169
.VueCarousel-navigation
17-
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', role='button', style='padding: 8px; margin-right: -8px;') &#x25C0;
10+
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', tabindex='0', style='padding: 8px; margin-right: -8px;') &#x25C0;
1811
|
19-
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', role='button', style='padding: 8px; margin-left: -8px;') &#x25B6;
12+
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', tabindex='-1', style='padding: 8px; margin-left: -8px;') &#x25B6;
13+
|
14+
.VueCarousel-pagination(style='')
15+
.VueCarousel-dot-container(role='tablist', style='margin-top: 20px;')
16+
button.VueCarousel-dot(aria-hidden='false', role='tab', title='Item 0', value='Item 0', aria-label='Item 0', aria-selected='false', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);')
17+
button.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='tab', title='Item 1', value='Item 1', aria-label='Item 1', aria-selected='true', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);')
2018
"
2119
`;
2220

2321
exports[`Navigation should render a next button 1`] = `
2422
".VueCarousel-wrapper
25-
.VueCarousel-inner(role='listbox', style='transform: translate(0px, 0); visibility: hidden; height: auto;')
26-
.VueCarousel-slide(tabindex='-1')
27-
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1')
28-
|
29-
.VueCarousel-pagination(style='')
30-
ul.VueCarousel-dot-container(role='tablist')
31-
li.VueCarousel-dot(aria-hidden='false', role='presentation', aria-selected='false', style='margin-top: 20px; padding: 10px;')
32-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 0', tabindex='0', style='width: 10px; height: 10px; background: rgb(239, 239, 239);')
33-
li.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='presentation', aria-selected='true', style='margin-top: 20px; padding: 10px;')
34-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 1', tabindex='0', style='width: 10px; height: 10px; background: rgb(0, 0, 0);')
23+
.VueCarousel-inner(style='transform: translate(0px, 0); visibility: hidden; height: auto;')
24+
.VueCarousel-slide(tabindex='-1', role='tabpanel', aria-hidden='true')
25+
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1', role='tabpanel')
3526
|
3627
.VueCarousel-navigation
37-
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', role='button', style='padding: 8px; margin-right: -8px;') &#x25C0;
28+
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', tabindex='0', style='padding: 8px; margin-right: -8px;') &#x25C0;
3829
|
39-
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', role='button', style='padding: 8px; margin-left: -8px;') &#x25B6;
30+
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', tabindex='-1', style='padding: 8px; margin-left: -8px;') &#x25B6;
31+
|
32+
.VueCarousel-pagination(style='')
33+
.VueCarousel-dot-container(role='tablist', style='margin-top: 20px;')
34+
button.VueCarousel-dot(aria-hidden='false', role='tab', title='Item 0', value='Item 0', aria-label='Item 0', aria-selected='false', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);')
35+
button.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='tab', title='Item 1', value='Item 1', aria-label='Item 1', aria-selected='true', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);')
4036
"
4137
`;
4238

4339
exports[`Navigation should render a prev button 1`] = `
4440
".VueCarousel-wrapper
45-
.VueCarousel-inner(role='listbox', style='transform: translate(0px, 0); visibility: hidden; height: auto;')
46-
.VueCarousel-slide(tabindex='-1')
47-
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1')
48-
|
49-
.VueCarousel-pagination(style='')
50-
ul.VueCarousel-dot-container(role='tablist')
51-
li.VueCarousel-dot(aria-hidden='false', role='presentation', aria-selected='false', style='margin-top: 20px; padding: 10px;')
52-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 0', tabindex='0', style='width: 10px; height: 10px; background: rgb(239, 239, 239);')
53-
li.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='presentation', aria-selected='true', style='margin-top: 20px; padding: 10px;')
54-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 1', tabindex='0', style='width: 10px; height: 10px; background: rgb(0, 0, 0);')
41+
.VueCarousel-inner(style='transform: translate(0px, 0); visibility: hidden; height: auto;')
42+
.VueCarousel-slide(tabindex='-1', role='tabpanel', aria-hidden='true')
43+
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1', role='tabpanel')
5544
|
5645
.VueCarousel-navigation
57-
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', role='button', style='padding: 8px; margin-right: -8px;') &#x25C0;
46+
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', tabindex='0', style='padding: 8px; margin-right: -8px;') &#x25C0;
5847
|
59-
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', role='button', style='padding: 8px; margin-left: -8px;') &#x25B6;
48+
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', tabindex='-1', style='padding: 8px; margin-left: -8px;') &#x25B6;
49+
|
50+
.VueCarousel-pagination(style='')
51+
.VueCarousel-dot-container(role='tablist', style='margin-top: 20px;')
52+
button.VueCarousel-dot(aria-hidden='false', role='tab', title='Item 0', value='Item 0', aria-label='Item 0', aria-selected='false', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);')
53+
button.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='tab', title='Item 1', value='Item 1', aria-label='Item 1', aria-selected='true', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);')
6054
"
6155
`;
6256

6357
exports[`Navigation should trigger page advance backward when prev is clicked 1`] = `
6458
".VueCarousel-wrapper
65-
.VueCarousel-inner(role='listbox', style='transform: translate(0px, 0); visibility: hidden; height: auto;')
66-
.VueCarousel-slide(tabindex='-1')
67-
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1')
68-
|
69-
.VueCarousel-pagination(style='')
70-
ul.VueCarousel-dot-container(role='tablist')
71-
li.VueCarousel-dot(aria-hidden='false', role='presentation', aria-selected='false', style='margin-top: 20px; padding: 10px;')
72-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 0', tabindex='0', style='width: 10px; height: 10px; background: rgb(239, 239, 239);')
73-
li.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='presentation', aria-selected='true', style='margin-top: 20px; padding: 10px;')
74-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 1', tabindex='0', style='width: 10px; height: 10px; background: rgb(0, 0, 0);')
59+
.VueCarousel-inner(style='transform: translate(0px, 0); visibility: hidden; height: auto;')
60+
.VueCarousel-slide(tabindex='-1', role='tabpanel', aria-hidden='true')
61+
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1', role='tabpanel')
7562
|
7663
.VueCarousel-navigation
77-
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', role='button', style='padding: 8px; margin-right: -8px;') &#x25C0;
64+
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', tabindex='0', style='padding: 8px; margin-right: -8px;') &#x25C0;
7865
|
79-
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', role='button', style='padding: 8px; margin-left: -8px;') &#x25B6;
66+
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', tabindex='-1', style='padding: 8px; margin-left: -8px;') &#x25B6;
67+
|
68+
.VueCarousel-pagination(style='')
69+
.VueCarousel-dot-container(role='tablist', style='margin-top: 20px;')
70+
button.VueCarousel-dot(aria-hidden='false', role='tab', title='Item 0', value='Item 0', aria-label='Item 0', aria-selected='false', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);')
71+
button.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='tab', title='Item 1', value='Item 1', aria-label='Item 1', aria-selected='true', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);')
8072
"
8173
`;
8274

8375
exports[`Navigation should trigger page advance when next is clicked 1`] = `
8476
".VueCarousel-wrapper
85-
.VueCarousel-inner(role='listbox', style='transform: translate(0px, 0); visibility: hidden; height: auto;')
86-
.VueCarousel-slide(tabindex='-1')
87-
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1')
88-
|
89-
.VueCarousel-pagination(style='')
90-
ul.VueCarousel-dot-container(role='tablist')
91-
li.VueCarousel-dot(aria-hidden='false', role='presentation', aria-selected='false', style='margin-top: 20px; padding: 10px;')
92-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 0', tabindex='0', style='width: 10px; height: 10px; background: rgb(239, 239, 239);')
93-
li.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='presentation', aria-selected='true', style='margin-top: 20px; padding: 10px;')
94-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 1', tabindex='0', style='width: 10px; height: 10px; background: rgb(0, 0, 0);')
77+
.VueCarousel-inner(style='transform: translate(0px, 0); visibility: hidden; height: auto;')
78+
.VueCarousel-slide(tabindex='-1', role='tabpanel', aria-hidden='true')
79+
.VueCarousel-slide.VueCarousel-slide-active.VueCarousel-slide-center(tabindex='-1', role='tabpanel')
9580
|
9681
.VueCarousel-navigation
97-
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', role='button', style='padding: 8px; margin-right: -8px;') &#x25C0;
82+
button.VueCarousel-navigation-button.VueCarousel-navigation-prev(type='button', aria-label='Previous page', tabindex='0', style='padding: 8px; margin-right: -8px;') &#x25C0;
9883
|
99-
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', role='button', style='padding: 8px; margin-left: -8px;') &#x25B6;
84+
button.VueCarousel-navigation-button.VueCarousel-navigation-next.VueCarousel-navigation--disabled(type='button', aria-label='Next page', tabindex='-1', style='padding: 8px; margin-left: -8px;') &#x25B6;
85+
|
86+
.VueCarousel-pagination(style='')
87+
.VueCarousel-dot-container(role='tablist', style='margin-top: 20px;')
88+
button.VueCarousel-dot(aria-hidden='false', role='tab', title='Item 0', value='Item 0', aria-label='Item 0', aria-selected='false', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(239, 239, 239);')
89+
button.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='tab', title='Item 1', value='Item 1', aria-label='Item 1', aria-selected='true', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);')
10090
"
10191
`;

tests/client/components/__snapshots__/slide.spec.js.snap

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
exports[`Slide should mount successfully 1`] = `
44
".VueCarousel-wrapper
5-
.VueCarousel-inner(role='listbox', style='transform: translate(0px, 0); visibility: hidden; height: auto;')
6-
.VueCarousel-slide(tabindex='-1')
7-
|
8-
.VueCarousel-pagination(style='display: none;')
9-
ul.VueCarousel-dot-container(role='tablist')
10-
li.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='presentation', aria-selected='true', style='margin-top: 20px; padding: 10px;')
11-
button.VueCarousel-dot-button(type='button', role='button', aria-label='\`Item \${index}\`', title='Item 0', tabindex='0', style='width: 10px; height: 10px; background: rgb(0, 0, 0);')
5+
.VueCarousel-inner(style='transform: translate(0px, 0); visibility: hidden; height: auto;')
6+
.VueCarousel-slide(tabindex='-1', aria-hidden='true', role='tabpanel')
127
//
8+
.VueCarousel-pagination(style='display: none;')
9+
.VueCarousel-dot-container(role='tablist', style='margin-top: 20px;')
10+
button.VueCarousel-dot.VueCarousel-dot--active(aria-hidden='false', role='tab', title='Item 0', value='Item 0', aria-label='Item 0', aria-selected='true', style='margin-top: 20px; padding: 10px; width: 10px; height: 10px; background-color: rgb(0, 0, 0);')
1311
"
1412
`;

0 commit comments

Comments
 (0)