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

This commit is contained in:
Eliot Berriot 2018-10-21 15:41:31 +02:00
parent a21db8d96a
commit ae55e6483d
No known key found for this signature in database
GPG Key ID: DD6965E2476E5C27
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; cursor: pointer;
} }
.ui.really.basic.button {
&:not(:focus) {
box-shadow: none !important;
background-color: none !important;
}
}
.floated.buttons .button ~ .dropdown { .floated.buttons .button ~ .dropdown {
border-left: none; border-left: none;
} }
@ -380,4 +387,27 @@ a {
display: none; 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> </style>

View File

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

View File

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

View File

@ -12,9 +12,9 @@
<div v-if="!discrete && !iconOnly" :class="['ui', {disabled: !playable}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]"> <div v-if="!discrete && !iconOnly" :class="['ui', {disabled: !playable}, 'floating', 'dropdown', {'icon': !dropdownOnly}, {'button': !dropdownOnly}]">
<i :class="dropdownIconClasses.concat(['icon'])"></i> <i :class="dropdownIconClasses.concat(['icon'])"></i>
<div class="menu"> <div class="menu">
<div class="item" :disabled="!playable" @click="add" :title="labels.addToQueue"><i class="plus icon"></i><translate>Add to queue</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>
<div class="item" :disabled="!playable" @click="addNext()" :title="labels.playNext"><i class="step forward icon"></i><translate>Play next</translate></div> <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>
<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="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>
</div> </div>
</span> </span>
@ -46,7 +46,16 @@ export default {
} }
}, },
mounted () { 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: { computed: {
labels () { labels () {
@ -139,6 +148,7 @@ export default {
this.getPlayableTracks().then((tracks) => { this.getPlayableTracks().then((tracks) => {
self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks)) self.$store.dispatch('queue/appendMany', {tracks: tracks}).then(() => self.addMessage(tracks))
}) })
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
}, },
addNext (next) { addNext (next) {
let self = this let self = this
@ -150,6 +160,7 @@ export default {
self.$store.dispatch('queue/next') self.$store.dispatch('queue/next')
} }
}) })
jQuery(self.$el).find('.ui.dropdown').dropdown('hide')
}, },
addMessage (tracks) { addMessage (tracks) {
if (tracks.length < 1) { if (tracks.length < 1) {
@ -170,4 +181,8 @@ export default {
i { i {
cursor: pointer; cursor: pointer;
} }
button.item {
background-color: white;
width: 100%;
}
</style> </style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,14 @@
<translate v-if="isFavorite">In favorites</translate> <translate v-if="isFavorite">In favorites</translate>
<translate v-else>Add to favorites</translate> <translate v-else>Add to favorites</translate>
</button> </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> </template>
<script> <script>

View File

@ -6,12 +6,14 @@
<i class="list icon"></i> <i class="list icon"></i>
<translate>Add to playlist...</translate> <translate>Add to playlist...</translate>
</button> </button>
<i <button
v-else v-else
@click="$store.commit('playlists/chooseTrack', track)" @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"> :title="labels.addToPlaylist">
</i> <i :class="['list', 'basic', 'icon']"></i>
</button>
</template> </template>
<script> <script>

View File

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