Merge branch '576-no-span-button' into 'develop'

Fix #576: Improved keyboard accessibility on player, queue and various controls

Closes #576

See merge request funkwhale/funkwhale!453
This commit is contained in:
Eliot Berriot 2018-10-21 14:03:55 +00:00
commit 7a5a7208bf
13 changed files with 185 additions and 82 deletions

View File

@ -0,0 +1 @@
Improved keyboard accessibility on player, queue and various controls (#576)

View File

@ -359,6 +359,13 @@ html, body {
cursor: pointer;
}
.ui.really.basic.button {
&:not(:focus) {
box-shadow: none !important;
background-color: none !important;
}
}
.floated.buttons .button ~ .dropdown {
border-left: none;
}
@ -380,4 +387,27 @@ a {
display: none;
}
button.reset {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
/* inherit font & color from ancestor */
color: inherit;
font: inherit;
/* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
line-height: normal;
/* Corrects font smoothing for webkit */
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
/* Corrects inability to style clickable `input` types in iOS */
-webkit-appearance: none;
text-align: inherit;
}
</style>

View File

@ -1,25 +1,25 @@
<template>
<div class="ui pagination menu">
<div
<a href
:disabled="current - 1 < 1"
@click="selectPage(current - 1)"
:class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></div>
@click.prevent.stop="selectPage(current - 1)"
:class="[{'disabled': current - 1 < 1}, 'item']"><i class="angle left icon"></i></a>
<template v-if="!compact">
<div
<a href
v-if="page !== 'skip'"
v-for="page in pages"
@click="selectPage(page)"
@click.prevent.stop="selectPage(page)"
:class="[{'active': page === current}, 'item']">
{{ page }}
</div>
</a href>
<div v-else class="disabled item">
...
</div>
</template>
<div
<a href
:disabled="current + 1 > maxPage"
@click="selectPage(current + 1)"
:class="[{'disabled': current + 1 > maxPage}, 'item']"><i class="angle right icon"></i></div>
@click.prevent.stop="selectPage(current + 1)"
:class="[{'disabled': current + 1 > maxPage}, 'item']"><i class="angle right icon"></i></a>
</div>
</template>
@ -90,4 +90,3 @@ export default {
cursor: pointer;
}
</style>

View File

@ -16,8 +16,8 @@
<div class="menu-area">
<div class="ui compact fluid two item inverted menu">
<a class="active item" @click="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
<a class="item" @click="selectedTab = 'queue'" data-tab="queue">
<a class="active item" href @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
<a class="item" href @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
<translate>Queue</translate>&nbsp;
<template v-if="queue.tracks.length === 0">
<translate>(empty)</translate>
@ -128,8 +128,10 @@
<img class="ui mini image" v-else src="../assets/audio/default-cover.png">
</td>
<td colspan="4">
<strong>{{ track.title }}</strong><br />
{{ track.artist.name }}
<button class="title reset ellipsis">
<strong>{{ track.title }}</strong><br />
{{ track.artist.name }}
</button>
</td>
<td>
<template v-if="$store.getters['favorites/isFavorite'](track.id)">
@ -137,7 +139,9 @@
</template>
</td>
<td>
<i @click.stop="cleanTrack(index)" class="circular trash icon"></i>
<button @click.stop="cleanTrack(index)" :class="['ui', {'inverted': index != queue.currentIndex}, 'really', 'tiny', 'basic', 'circular', 'icon', 'button']">
<i class="trash icon"></i>
</button>
</td>
</tr>
</draggable>
@ -428,4 +432,8 @@ $sidebar-color: #3d3e3f;
top: -0.5em;
width: 3em;
}
:not(.active) button.title {
outline-color: white;
}
</style>

View File

@ -12,9 +12,9 @@
<div v-if="!discrete && !iconOnly" :class="['ui', {disabled: !playable}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]">
<i :class="dropdownIconClasses.concat(['icon'])"></i>
<div class="menu">
<div class="item" :disabled="!playable" @click="add" :title="labels.addToQueue"><i class="plus icon"></i><translate>Add to queue</translate></div>
<div class="item" :disabled="!playable" @click="addNext()" :title="labels.playNext"><i class="step forward icon"></i><translate>Play next</translate></div>
<div class="item" :disabled="!playable" @click="addNext(true)" :title="labels.playNow"><i class="play icon"></i><translate>Play now</translate></div>
<button class="item basic" ref="add" data-ref="add" :disabled="!playable" @click.stop.prevent="add" :title="labels.addToQueue"><i class="plus icon"></i><translate>Add to queue</translate></button>
<button class="item basic" ref="addNext" data-ref="addNext" :disabled="!playable" @click.stop.prevent="addNext()" :title="labels.playNext"><i class="step forward icon"></i><translate>Play next</translate></button>
<button class="item basic" ref="playNow" data-ref="playNow" :disabled="!playable" @click.stop.prevent="addNext(true)" :title="labels.playNow"><i class="play icon"></i><translate>Play now</translate></button>
</div>
</div>
</span>
@ -46,7 +46,16 @@ export default {
}
},
mounted () {
jQuery(this.$el).find('.ui.dropdown').dropdown()
let self = this
jQuery(this.$el).find('.ui.dropdown').dropdown({
selectOnKeydown: false,
action: function (text, value, $el) {
// used ton ensure focusing the dropdown and clicking via keyboard
// works as expected
self.$refs[$el.data('ref')].click()
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
}
})
},
computed: {
labels () {
@ -139,6 +148,7 @@ export default {
this.getPlayableTracks().then((tracks) => {
self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks))
})
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
},
addNext (next) {
let self = this
@ -150,6 +160,7 @@ export default {
self.$store.dispatch('queue/next')
}
})
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
},
addMessage (tracks) {
if (tracks.length < 1) {
@ -170,4 +181,8 @@ export default {
i {
cursor: pointer;
}
button.item {
background-color: white;
width: 100%;
}
</style>

View File

@ -31,9 +31,11 @@
<div class="description">
<track-favorite-icon
v-if="$store.state.auth.authenticated"
:class="{'inverted': !$store.getters['favorites/isFavorite'](currentTrack.id)}"
:track="currentTrack"></track-favorite-icon>
<track-playlist-icon
v-if="$store.state.auth.authenticated"
:class="['inverted']"
:track="currentTrack"></track-playlist-icon>
</div>
</div>
@ -55,44 +57,71 @@
</div>
<div class="two wide column controls ui grid">
<div
<a
href
:title="labels.previousTrack"
:aria-label="labels.previousTrack"
class="two wide column control"
@click.prevent.stop="previous"
:disabled="emptyQueue">
<i @click="previous" :class="['ui', 'backward', {'disabled': emptyQueue}, 'secondary', 'icon']"></i>
</div>
<div
<i :class="['ui', 'backward', {'disabled': emptyQueue}, 'secondary', 'icon']"></i>
</a>
<a
href
v-if="!playing"
:title="labels.play"
:aria-label="labels.play"
@click.prevent.stop="togglePlay"
class="two wide column control">
<i @click="togglePlay" :class="['ui', 'play', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
</div>
<div
<i :class="['ui', 'play', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
</a>
<a
href
v-else
:title="labels.pause"
:aria-label="labels.pause"
@click.prevent.stop="togglePlay"
class="two wide column control">
<i @click="togglePlay" :class="['ui', 'pause', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
</div>
<div
<i :class="['ui', 'pause', {'disabled': !currentTrack}, 'secondary', 'icon']"></i>
</a>
<a
href
:title="labels.next"
:aria-label="labels.next"
class="two wide column control"
@click.prevent.stop="next"
:disabled="!hasNext">
<i @click="next" :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'secondary', 'icon']" ></i>
</div>
<i :class="['ui', {'disabled': !hasNext}, 'step', 'forward', 'secondary', 'icon']" ></i>
</a>
<div
class="wide column control volume-control"
v-on:mouseover="showVolume = true"
v-on:mouseleave="showVolume = false"
v-bind:class="{ active : showVolume }">
<i
<a
href
v-if="volume === 0"
:title="labels.unmute"
@click="unmute" v-if="volume === 0" class="volume off secondary icon"></i>
<i
:aria-label="labels.unmute"
@click.prevent.stop="unmute">
<i class="volume off secondary icon"></i>
</a>
<a
href
v-else-if="volume < 0.5"
:title="labels.mute"
@click="mute" v-else-if="volume < 0.5" class="volume down secondary icon"></i>
<i
:aria-label="labels.mute"
@click.prevent.stop="mute">
<i class="volume down secondary icon"></i>
</a>
<a
href
v-else
:title="labels.mute"
@click="mute" v-else class="volume up secondary icon"></i>
:aria-label="labels.mute"
@click.prevent.stop="mute">
<i class="volume up secondary icon"></i>
</a>
<input
type="range"
step="0.05"
@ -102,44 +131,61 @@
v-if="showVolume" />
</div>
<div class="two wide column control looping" v-if="!showVolume">
<i
:title="labels.loopingDisabled"
<a
href
v-if="looping === 0"
@click="$store.commit('player/looping', 1)"
:disabled="!currentTrack"
:class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
<i
:title="labels.loopingDisabled"
:aria-label="labels.loopingDisabled"
@click.prevent.stop="$store.commit('player/looping', 1)"
:disabled="!currentTrack">
<i :class="['ui', {'disabled': !currentTrack}, 'step', 'repeat', 'secondary', 'icon']"></i>
</a>
<a
href
@click.prevent.stop="$store.commit('player/looping', 2)"
:title="labels.loopingSingle"
:aria-label="labels.loopingSingle"
v-if="looping === 1"
@click="$store.commit('player/looping', 2)"
:disabled="!currentTrack"
class="repeat secondary icon">
<span class="ui circular tiny orange label">1</span>
</i>
<i
:disabled="!currentTrack">
<i
class="repeat secondary icon">
<span class="ui circular tiny orange label">1</span>
</i>
</a>
<a
href
:title="labels.loopingWhole"
:aria-label="labels.loopingWhole"
v-if="looping === 2"
@click="$store.commit('player/looping', 0)"
:disabled="!currentTrack"
class="repeat orange secondary icon">
</i>
@click.prevent.stop="$store.commit('player/looping', 0)">
<i
class="repeat orange secondary icon">
</i>
</a>
</div>
<div
<a
href
:disabled="queue.tracks.length === 0"
:title="labels.shuffle"
:aria-label="labels.shuffle"
v-if="!showVolume"
@click.prevent.stop="shuffle()"
class="two wide column control">
<div v-if="isShuffling" class="ui inline shuffling inverted tiny active loader"></div>
<i v-else @click="shuffle()" :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</div>
<i v-else :class="['ui', 'random', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</a>
<div class="one wide column" v-if="!showVolume"></div>
<div
<a
href
:disabled="queue.tracks.length === 0"
:title="labels.clear"
:aria-label="labels.clear"
v-if="!showVolume"
@click.prevent.stop="clean()"
class="two wide column control">
<i @click="clean()" :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</div>
<i :class="['ui', 'trash', 'secondary', {'disabled': queue.tracks.length === 0}, 'icon']" ></i>
</a>
</div>
<GlobalEvents
@keydown.space.prevent.exact="togglePlay"
@ -147,7 +193,6 @@
@keydown.ctrl.right.prevent.exact="next"
@keydown.ctrl.down.prevent.exact="$store.commit('player/incrementVolume', -0.1)"
@keydown.ctrl.up.prevent.exact="$store.commit('player/incrementVolume', 0.1)"
@keydown.f.prevent.exact="$store.dispatch('favorites/toggle', currentTrack.id)"
@keydown.l.prevent.exact="$store.commit('player/toggleLooping')"
@keydown.s.prevent.exact="shuffle"
/>
@ -370,6 +415,9 @@ export default {
color: white !important;
}
}
.controls a {
color: white;
}
.controls .icon.big {
cursor: pointer;

View File

@ -3,10 +3,8 @@
<h3 class="ui header">
<slot name="title"></slot>
</h3>
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle left', 'icon']">
</i>
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle right', 'icon']">
</i>
<button :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle left', 'icon']"></i></button>
<button :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle right', 'icon']"></i></button>
<div class="ui hidden divider"></div>
<div class="ui five cards">
<div v-if="isLoading" class="ui inverted active dimmer">

View File

@ -40,7 +40,7 @@
<td colspan="4" v-else>
<translate>N/A</translate>
</td>
<td>
<td colspan="2">
<track-favorite-icon class="favorite-icon" :track="track"></track-favorite-icon>
<track-playlist-icon
v-if="$store.state.auth.authenticated"
@ -89,7 +89,7 @@ export default {
tr:not(:hover) {
.favorite-icon:not(.favorited), .playlist-icon {
display: none;
visibility: hidden;
}
}
</style>

View File

@ -8,7 +8,7 @@
<th colspan="4"><translate>Artist</translate></th>
<th colspan="4"><translate>Album</translate></th>
<th colspan="4"><translate>Duration</translate></th>
<th></th>
<th colspan="2"></th>
</tr>
</thead>
<tbody>

View File

@ -3,12 +3,9 @@
<h3 class="ui header">
<slot name="title"></slot>
</h3>
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle up', 'icon']">
</i>
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle down', 'icon']">
</i>
<i @click="fetchData(url)" :class="['ui', 'circular', 'medium', 'refresh', 'icon']">
</i>
<button :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle up', 'icon']"></i></button>
<button :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle down', 'icon']"></i></button>
<button @click="fetchData(url)" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
<div class="ui divided unstackable items">
<div class="item" v-for="object in objects" :key="object.id">
<div class="ui tiny image">

View File

@ -4,7 +4,14 @@
<translate v-if="isFavorite">In favorites</translate>
<translate v-else>Add to favorites</translate>
</button>
<i v-else @click="$store.dispatch('favorites/toggle', track.id)" :class="['favorite-icon', 'heart', {'pink': isFavorite}, {'favorited': isFavorite}, 'link', 'icon']" :title="title"></i>
<button
v-else
@click="$store.dispatch('favorites/toggle', track.id)"
:class="['ui', 'favorite-icon', {'pink': isFavorite}, {'favorited': isFavorite}, 'basic', 'circular', 'icon', 'really', 'button']"
:aria-label="title"
:title="title">
<i :class="['heart', {'pink': isFavorite}, 'basic', 'icon']"></i>
</button>
</template>
<script>

View File

@ -6,12 +6,14 @@
<i class="list icon"></i>
<translate>Add to playlist...</translate>
</button>
<i
<button
v-else
@click="$store.commit('playlists/chooseTrack', track)"
:class="['playlist-icon', 'list', 'link', 'icon']"
:class="['ui', 'basic', 'circular', 'icon', 'really', 'button']"
:aria-label="labels.addToPlaylist"
:title="labels.addToPlaylist">
</i>
<i :class="['list', 'basic', 'icon']"></i>
</button>
</template>
<script>

View File

@ -3,12 +3,10 @@
<h3 class="ui header">
<slot name="title"></slot>
</h3>
<i @click="fetchData(previousPage)" :disabled="!previousPage" :class="['ui', {disabled: !previousPage}, 'circular', 'medium', 'angle up', 'icon']">
</i>
<i @click="fetchData(nextPage)" :disabled="!nextPage" :class="['ui', {disabled: !nextPage}, 'circular', 'medium', 'angle down', 'icon']">
</i>
<i @click="fetchData(url)" :class="['ui', 'circular', 'medium', 'refresh', 'icon']">
</i>
<button :disabled="!previousPage" @click="fetchData(previousPage)" :class="['ui', {disabled: !previousPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle up', 'icon']"></i></button>
<button :disabled="!nextPage" @click="fetchData(nextPage)" :class="['ui', {disabled: !nextPage}, 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'angle down', 'icon']"></i></button>
<button @click="fetchData(url)" :class="['ui', 'circular', 'icon', 'basic', 'button']"><i :class="['ui', 'refresh', 'icon']"></i></button>
<div v-if="isLoading" class="ui inverted active dimmer">
<div class="ui loader"></div>
</div>