diff --git a/changes/changelog.d/576.enhancement b/changes/changelog.d/576.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..ee6087a73d77db263119bb4c8b0a4f0617ceb262 --- /dev/null +++ b/changes/changelog.d/576.enhancement @@ -0,0 +1 @@ +Improved keyboard accessibility on player, queue and various controls (#576) diff --git a/front/src/App.vue b/front/src/App.vue index 16154b130d2d49f9c57b3f35e6daeba7b5688b61..946a0621e25c8dc8459a3336bf48bdc703a58243 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -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> diff --git a/front/src/components/Pagination.vue b/front/src/components/Pagination.vue index cc5f171649fb6fa5832832f90ba11a3d93c79a9b..bdc20b53d9884f4f1a24e3ac1655c4d6f1bd531f 100644 --- a/front/src/components/Pagination.vue +++ b/front/src/components/Pagination.vue @@ -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> - diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue index efd6f416cfaaf3f6ce99d77a66da88211e66875d..8702f80511592922e24896190b15f6964e799528 100644 --- a/front/src/components/Sidebar.vue +++ b/front/src/components/Sidebar.vue @@ -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> <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> diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index 600d3de4cb2bc353f3cbc3bd18b53f659cb4e041..dcb1c507e121441aa9712aaca403f8dcc5759861 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -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> diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index 9c918a5fa919753f491318aa7506386585674e89..5387d02b753e4b5626d637eaf78a57a4b5814cb9 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -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; diff --git a/front/src/components/audio/album/Widget.vue b/front/src/components/audio/album/Widget.vue index 73bf0d226949dcd63ad530c25c2f4e1e91e27a8d..ccef492955f7a1b0499686993cc8bcece202a630 100644 --- a/front/src/components/audio/album/Widget.vue +++ b/front/src/components/audio/album/Widget.vue @@ -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"> diff --git a/front/src/components/audio/track/Row.vue b/front/src/components/audio/track/Row.vue index 2dce036ebabd73de165b6de841770e67ff1af587..b17cf117073190f767e82894de09d12106712d47 100644 --- a/front/src/components/audio/track/Row.vue +++ b/front/src/components/audio/track/Row.vue @@ -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> diff --git a/front/src/components/audio/track/Table.vue b/front/src/components/audio/track/Table.vue index 476a3f0bdbebf131b19158c5f0dd9ed4b4bb2276..9612accbc39d89fcbe569ff7be9c9dd020439430 100644 --- a/front/src/components/audio/track/Table.vue +++ b/front/src/components/audio/track/Table.vue @@ -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> diff --git a/front/src/components/audio/track/Widget.vue b/front/src/components/audio/track/Widget.vue index 6a1d066904129b89dea2ceb99fcc4bcf41c8c984..5ed28098102152a55f893b8d914168e1f4db4a2b 100644 --- a/front/src/components/audio/track/Widget.vue +++ b/front/src/components/audio/track/Widget.vue @@ -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"> diff --git a/front/src/components/favorites/TrackFavoriteIcon.vue b/front/src/components/favorites/TrackFavoriteIcon.vue index 88402159c934b06ca7b6c5514ed9babbd5ef1b96..690dab21b67f0f2e742ed236564331dbfabfcfca 100644 --- a/front/src/components/favorites/TrackFavoriteIcon.vue +++ b/front/src/components/favorites/TrackFavoriteIcon.vue @@ -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> diff --git a/front/src/components/playlists/TrackPlaylistIcon.vue b/front/src/components/playlists/TrackPlaylistIcon.vue index a75c217f4419935ee807fd7460404fcf756a677e..58b06ccf5ee3caca715dd41ad2a89a521496e7c9 100644 --- a/front/src/components/playlists/TrackPlaylistIcon.vue +++ b/front/src/components/playlists/TrackPlaylistIcon.vue @@ -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> diff --git a/front/src/components/playlists/Widget.vue b/front/src/components/playlists/Widget.vue index 65904a12447fc0a7d9e0250bbc987f1f286b72e8..868719f1c2536b4b480c4ca0fd3365415222a88a 100644 --- a/front/src/components/playlists/Widget.vue +++ b/front/src/components/playlists/Widget.vue @@ -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>