Skip to content

Commit

Permalink
Improve Tabs a11y (buefy#2744)
Browse files Browse the repository at this point in the history
  • Loading branch information
service-paradis authored Mar 29, 2021
1 parent 11a23d7 commit 1c76ed8
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 32 deletions.
3 changes: 3 additions & 0 deletions docs/pages/components/tabs/Tabs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@
<Example :component="ExVertical" :code="ExVerticalCode" title="Vertical"/>

<ApiView :data="api"/>
<VariablesView :data="variables"/>
</div>
</template>

<script>
import api from './api/tabs'
import variables from './variables/tabs'
import ExSimple from './examples/ExSimple'
import ExSimpleCode from '!!raw-loader!./examples/ExSimple'
Expand Down Expand Up @@ -68,6 +70,7 @@
data() {
return {
api,
variables,
ExSimple,
ExDynamic,
ExPosition,
Expand Down
46 changes: 46 additions & 0 deletions docs/pages/components/tabs/variables/tabs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export default [
{
name: '<code>$tabs-focused-outline</code>',
default: '<code>none</code>'
},
{
name: '<code>$tabs-link-focus-active-border-bottom-color</code>',
default: '<code>$tabs-link-active-border-bottom-color</code>'
},
{
name: '<code>$tabs-link-focus-border-bottom-color</code>',
default: '<code>$tabs-link-hover-border-bottom-color</code>'
},
{
name: '<code>$tabs-boxed-link-focus-active-background-color</code>',
default: '<code>$tabs-boxed-link-active-background-color</code>'
},
{
name: '<code>$tabs-boxed-link-focus-background-color</code>',
default: '<code>$tabs-boxed-link-hover-background-color</code>'
},
{
name: '<code>$tabs-boxed-link-focus-active-border-bottom-color</code>',
default: '<code>$tabs-boxed-link-active-border-bottom-color</code>'
},
{
name: '<code>$tabs-boxed-link-focus-border-bottom-color</code>',
default: '<code>$tabs-boxed-link-hover-border-bottom-color</code>'
},
{
name: '<code>$tabs-toggle-link-focus-active-background-color</code>',
default: '<code>$tabs-toggle-link-active-background-color</code>'
},
{
name: '<code>$tabs-toggle-link-focus-background-color</code>',
default: '<code>$tabs-toggle-link-hover-background-color</code>'
},
{
name: '<code>$tabs-toggle-link-focus-active-border-color</code>',
default: '<code>$tabs-toggle-link-active-border-color</code>'
},
{
name: '<code>$tabs-toggle-link-focus-border-color</code>',
default: '<code>$tabs-toggle-link-hover-border-color</code>'
}
]
36 changes: 23 additions & 13 deletions src/components/steps/Steps.vue
Original file line number Diff line number Diff line change
Expand Up @@ -157,36 +157,46 @@ export default {
* Check if previous button is available.
*/
hasPrev() {
return !!this.prevItem
return this.prevItemIdx !== null
},
/**
* Retrieves the next visible item index
*/
nextItemIdx() {
let idx = this.activeItem ? this.items.indexOf(this.activeItem) : 0
return this.getNextItemIdx(idx)
},
/**
* Retrieves the next visible item
*/
nextItem() {
let nextItem = null
let idx = this.activeItem ? this.items.indexOf(this.activeItem) + 1 : 0
for (; idx < this.items.length; idx++) {
if (this.items[idx].visible) {
nextItem = this.items[idx]
break
}
if (this.nextItemIdx !== null) {
nextItem = this.items[this.nextItemIdx]
}
return nextItem
},
/**
* Retrieves the next visible item index
*/
prevItemIdx() {
if (!this.activeItem) { return null }
let idx = this.items.indexOf(this.activeItem)
return this.getPrevItemIdx(idx)
},
/**
* Retrieves the previous visible item
*/
prevItem() {
if (!this.activeItem) { return null }
let prevItem = null
for (let idx = this.items.indexOf(this.activeItem) - 1; idx >= 0; idx--) {
if (this.items[idx].visible) {
prevItem = this.items[idx]
break
}
if (this.prevItemIdx !== null) {
prevItem = this.items[this.prevItemIdx]
}
return prevItem
},
Expand All @@ -195,7 +205,7 @@ export default {
* Check if next button is available.
*/
hasNext() {
return !!this.nextItem
return this.nextItemIdx !== null
},
navigationProps() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/steps/__snapshots__/StepItem.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`BStepItem render correctly 1`] = `<div class="step-item" style="display: none;"></div>`;
exports[`BStepItem render correctly 1`] = `<div class="step-item" id="16-content" tabindex="-1" style="display: none;"></div>`;
6 changes: 3 additions & 3 deletions src/components/steps/__snapshots__/Steps.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ exports[`BSteps render correctly 1`] = `
</ul>
</nav>
<section class="step-content">
<div class="step-item" style="display: none;"></div>
<div class="step-item" style="display: none;"></div>
<div class="step-item" style="display: none;"></div>
<div class="step-item" id="14-content" tabindex="-1" style="display: none;"></div>
<div class="step-item" id="16-content" tabindex="0" style="display: none;"></div>
<div class="step-item" id="18-content" tabindex="-1" style="display: none;"></div>
</section>
<nav class="step-navigation"><a role="button" class="pagination-previous"><span class="icon" aria-hidden="true"><i class="mdi mdi-chevron-left mdi-24px"></i></span></a> <a role="button" class="pagination-next"><span class="icon" aria-hidden="true"><i class="mdi mdi-chevron-right mdi-24px"></i></span></a></nav>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/components/tabs/TabItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export default {
},
data() {
return {
elementClass: 'tab-item'
elementClass: 'tab-item',
elementRole: 'tabpanel'
}
}
}
Expand Down
108 changes: 103 additions & 5 deletions src/components/tabs/Tabs.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
<template>
<div class="b-tabs" :class="mainClasses">
<nav class="tabs" :class="navClasses">
<nav
class="tabs"
:class="navClasses"
role="tablist"
:aria-orientation="vertical ? 'vertical' : 'horizontal'"
@keydown="manageTablistKeydown"
>
<ul>
<li
v-for="childItem in items"
v-for="(childItem, childIdx) in items"
:key="childItem.value"
v-show="childItem.visible"
:class="[ childItem.headerClass, { 'is-active': childItem.isActive,
'is-disabled': childItem.disabled }]">
'is-disabled': childItem.disabled }]"
role="presentation"
>
<b-slot-component
ref="tabLink"
v-if="childItem.$scopedSlots.header"
:component="childItem"
name="header"
tag="a"
role="tab"
:id="`${childItem.value}-label`"
:aria-controls="`${childItem.value}-content`"
:aria-selected="`${childItem.isActive}`"
:tabindex="childItem.isActive ? 0 : -1"
@focus.native="currentFocus = childIdx"
@click.native="childClick(childItem)"
@keydown="manageTabKeydown($event, childItem)"
/>
<a v-else @click="childClick(childItem)">
<a
ref="tabLink"
v-else
role="tab"
:id="`${childItem.value}-tab`"
:aria-controls="`${childItem.value}-content`"
:aria-selected="`${childItem.isActive}`"
:tabindex="childItem.isActive ? 0 : -1"
@focus="currentFocus = childIdx"
@click="childClick(childItem)"
@keydown="manageTabKeydown($event, childItem)"
>
<b-icon
v-if="childItem.icon"
:icon="childItem.icon"
Expand Down Expand Up @@ -60,6 +87,11 @@ export default {
},
multiline: Boolean
},
data() {
return {
currentFocus: this.value
}
},
computed: {
mainClasses() {
return {
Expand All @@ -76,10 +108,76 @@ export default {
{
[this.position]: this.position && !this.vertical,
'is-fullwidth': this.expanded,
'is-toggle-rounded is-toggle': this.type === 'is-toggle-rounded'
'is-toggle': this.type === 'is-toggle-rounded'
}
]
}
},
methods: {
giveFocusToTab(tab) {
if (tab.$el && tab.$el.focus) {
tab.$el.focus()
} else if (tab.focus) {
tab.focus()
}
},
manageTablistKeydown(event) {
// https://developer.mozilla.org/fr/docs/Web/API/KeyboardEvent/key/Key_Values#Navigation_keys
const { key } = event
switch (key) {
case this.vertical ? 'ArrowUp' : 'ArrowLeft':
case this.vertical ? 'Up' : 'Left': {
let prevIdx = this.getPrevItemIdx(this.currentFocus, true)
if (prevIdx === null) {
// We try to give focus back to the last visible element
prevIdx = this.getPrevItemIdx(this.items.length, true)
}
if (
prevIdx !== null &&
this.$refs.tabLink &&
prevIdx < this.$refs.tabLink.length &&
!this.items[prevIdx].disabled
) {
this.giveFocusToTab(this.$refs.tabLink[prevIdx])
}
event.preventDefault()
break
}
case this.vertical ? 'ArrowDown' : 'ArrowRight':
case this.vertical ? 'Down' : 'Right': {
let nextIdx = this.getNextItemIdx(this.currentFocus, true)
if (nextIdx === null) {
// We try to give focus back to the first visible element
nextIdx = this.getNextItemIdx(-1, true)
}
if (
nextIdx !== null &&
this.$refs.tabLink &&
nextIdx < this.$refs.tabLink.length &&
!this.items[nextIdx].disabled
) {
this.giveFocusToTab(this.$refs.tabLink[nextIdx])
}
event.preventDefault()
break
}
}
},
manageTabKeydown(event, childItem) {
// https://developer.mozilla.org/fr/docs/Web/API/KeyboardEvent/key/Key_Values#Navigation_keys
const { key } = event
switch (key) {
case ' ':
case 'Space':
case 'Spacebar':
case 'Enter': {
this.childClick(childItem)
event.preventDefault()
break
}
}
}
}
}
</script>
2 changes: 1 addition & 1 deletion src/components/tabs/__snapshots__/TabItem.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`BTabItem render correctly 1`] = `<div class="tab-item" style="display: none;"></div>`;
exports[`BTabItem render correctly 1`] = `<div class="tab-item" role="tabpanel" id="tab2-content" aria-labelledby="tab2-label" tabindex="-1" style="display: none;"></div>`;
12 changes: 6 additions & 6 deletions src/components/tabs/__snapshots__/Tabs.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@

exports[`BTabs render correctly 1`] = `
<div class="b-tabs">
<nav class="tabs">
<nav role="tablist" aria-orientation="horizontal" class="tabs">
<ul>
<li class="is-active"><a>
<li role="presentation" class="is-active"><a role="tab" id="tab1-tab" aria-controls="tab1-content" aria-selected="true" tabindex="0">
<!----> <span></span></a></li>
<li class="" style="display: none;"><a>
<li role="presentation" class="" style="display: none;"><a role="tab" id="tab2-tab" aria-controls="tab2-content" aria-selected="false" tabindex="-1">
<!----> <span></span></a></li>
</ul>
</nav>
<section class="tab-content">
<div class="tab-item"></div>
<div class="tab-item" style="display: none;"></div>
<div class="tab-item" role="tabpanel" id="tab1-content" aria-labelledby="tab1-label" tabindex="0"></div>
<div class="tab-item" role="tabpanel" id="tab2-content" aria-labelledby="tab2-label" tabindex="-1" style="display: none;"></div>
</section>
</div>
`;

exports[`BTabs still renders if there is no item 1`] = `
<div class="b-tabs">
<nav class="tabs">
<nav role="tablist" aria-orientation="horizontal" class="tabs">
<ul></ul>
</nav>
<section class="tab-content"></section>
Expand Down
Loading

0 comments on commit 1c76ed8

Please sign in to comment.