Verified Commit 29171853 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Fix #612: Improved accessibility by using main/section/nav tags and...

Fix #612: Improved accessibility by using main/section/nav tags and aria-labels in most critical places
parent 9005ebbd
Improved accessibility by using main/section/nav tags and aria-labels in most critical places (#612)
<template>
<div class="main pusher" v-title="labels.title">
<div class="ui vertical center aligned stripe segment">
<main class="main pusher" v-title="labels.title">
<section class="ui vertical center aligned stripe segment">
<div class="ui text container">
<h1 class="ui huge header">
<translate v-if="instance.name.value" :translate-params="{instance: instance.name.value}">
......@@ -10,8 +10,8 @@
</h1>
<stats></stats>
</div>
</div>
<div class="ui vertical stripe segment">
</section>
<section class="ui vertical stripe segment">
<p v-if="!instance.short_description.value && !instance.long_description.value">
<translate>Unfortunately, owners of this instance did not yet take the time to complete this page.</translate>
</p>
......@@ -31,28 +31,28 @@
class="ui middle aligned stackable text container"
v-html="$options.filters.markdown(instance.long_description.value)">
</div>
</div>
</div>
</section>
</main>
</template>
<script>
import {mapState} from 'vuex'
import Stats from '@/components/instance/Stats'
import { mapState } from "vuex"
import Stats from "@/components/instance/Stats"
export default {
components: {
Stats
},
created () {
this.$store.dispatch('instance/fetchSettings')
created() {
this.$store.dispatch("instance/fetchSettings")
},
computed: {
...mapState({
instance: state => state.instance.settings.instance
}),
labels () {
labels() {
return {
title: this.$gettext('About this instance')
title: this.$gettext("About this instance")
}
}
}
......
<template>
<footer id="footer" class="ui vertical footer segment">
<footer id="footer" role="contentinfo" class="ui vertical footer segment">
<div class="ui container">
<div class="ui stackable equal height stackable grid">
<div class="four wide column">
<section class="four wide column">
<h4 v-translate class="ui header">
<translate :translate-params="{instanceName: instanceHostname}" >About %{instanceName}</translate>
</h4>
......@@ -25,24 +25,24 @@
</select>
</div>
</div>
</div>
<div class="four wide column">
</section>
<section class="four wide column">
<h4 v-translate class="ui header">Using Funkwhale</h4>
<div class="ui link list">
<a href="https://docs.funkwhale.audio" class="item" target="_blank"><translate>Documentation</translate></a>
<a href="https://docs.funkwhale.audio/users/apps.html" class="item" target="_blank"><translate>Mobile and desktop apps</translate></a>
<div role="button" class="item" @click="$emit('show:shortcuts-modal')"><translate>Keyboard shortcuts</translate></div>
</div>
</div>
<div class="four wide column">
</section>
<section class="four wide column">
<h4 v-translate class="ui header">Getting help</h4>
<div class="ui link list">
<a href="https://socialhub.network/c/projects/funkwhale" class="item" target="_blank"><translate>Support forum</translate></a>
<a href="https://riot.im/app/#/room/#funkwhale-troubleshooting:matrix.org" class="item" target="_blank"><translate>Chat room</translate></a>
<a href="https://code.eliotberriot.com/funkwhale/funkwhale/issues" class="item" target="_blank"><translate>Issue tracker</translate></a>
</div>
</div>
<div class="four wide column">
</section>
<section class="four wide column">
<h4 v-translate class="ui header">About Funkwhale</h4>
<div class="ui link list">
<a href="https://funkwhale.audio" class="item" target="_blank"><translate>Official website</translate></a>
......@@ -53,45 +53,51 @@
<p>
<translate>The funkwhale logo was kindly designed and provided by Francis Gading.</translate>
</p>
</div>
</section>
</div>
</div>
</footer>
</template>
<script>
import {mapState} from 'vuex'
import { mapState } from "vuex"
export default {
props: ['version'],
props: ["version"],
methods: {
switchInstance () {
let confirm = window.confirm(this.$gettext('This will erase your local data and disconnect you, do you want to continue?'))
switchInstance() {
let confirm = window.confirm(
this.$gettext(
"This will erase your local data and disconnect you, do you want to continue?"
)
)
if (confirm) {
this.$store.commit('instance/instanceUrl', null)
this.$store.commit("instance/instanceUrl", null)
}
},
}
},
computed: {
...mapState({
messages: state => state.ui.messages
}),
instanceHostname () {
instanceHostname() {
let url = this.$store.state.instance.instanceUrl
let parser = document.createElement('a');
let parser = document.createElement("a")
parser.href = url
return parser.hostname
},
suggestedInstances () {
let instances = [this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio']
suggestedInstances() {
let instances = [
this.$store.getters["instance/defaultUrl"](),
"https://demo.funkwhale.audio"
]
return instances
},
}
}
}
</script>
<style scoped>
footer p {
color: grey;
}
footer p {
color: grey;
}
</style>
<template>
<div class="main pusher" v-title="labels.title">
<div class="ui vertical center aligned stripe segment">
<main class="main pusher" v-title="labels.title">
<section class="ui vertical center aligned stripe segment">
<div class="ui text container">
<h1 class="ui huge header">
<translate>Welcome on Funkwhale</translate>
......@@ -15,8 +15,8 @@
<i class="right arrow icon"></i>
</router-link>
</div>
</div>
<div class="ui vertical stripe segment">
</section>
<section class="ui vertical stripe segment">
<div class="ui middle aligned stackable text container">
<div class="ui grid">
<div class="row">
......@@ -136,22 +136,21 @@
</div>
</div>
</div>
</div>
</div>
</section>
</main>
</template>
<script>
export default {
data () {
data() {
return {
musicbrainzUrl: 'https://musicbrainz.org/'
musicbrainzUrl: "https://musicbrainz.org/"
}
},
computed: {
labels () {
labels() {
return {
title: this.$gettext('Welcome')
title: this.$gettext("Welcome")
}
}
}
......
<template>
<div class="main pusher" :v-title="labels.title">
<div class="ui vertical stripe segment">
<main class="main pusher" :v-title="labels.title">
<section class="ui vertical stripe segment">
<div class="ui text container">
<h1 class="ui huge header">
<i class="warning icon"></i>
......@@ -16,21 +16,21 @@
<i class="right arrow icon"></i>
</router-link>
</div>
</div>
</div>
</section>
</main>
</template>
<script>
export default {
data: function () {
data: function() {
return {
path: window.location.href
}
},
computed: {
labels () {
labels() {
return {
title: this.$gettext('Page Not Found')
title: this.$gettext("Page Not Found")
}
}
}
......
<template>
<div class="ui pagination menu">
<div class="ui pagination menu" role="navigation" :aria-label="labels.pagination">
<a href
:disabled="current - 1 < 1"
@click.prevent.stop="selectPage(current - 1)"
......@@ -24,30 +24,42 @@
</template>
<script>
import _ from 'lodash'
import _ from "lodash"
export default {
props: {
current: {type: Number, default: 1},
paginateBy: {type: Number, default: 25},
total: {type: Number},
compact: {type: Boolean, default: false}
current: { type: Number, default: 1 },
paginateBy: { type: Number, default: 25 },
total: { type: Number },
compact: { type: Boolean, default: false }
},
computed: {
pages: function () {
labels() {
return {
pagination: this.$gettext("Pagination")
}
},
pages: function() {
let range = 2
let current = this.current
let beginning = _.range(1, Math.min(this.maxPage, 1 + range))
let middle = _.range(Math.max(1, current - range + 1), Math.min(this.maxPage, current + range))
let middle = _.range(
Math.max(1, current - range + 1),
Math.min(this.maxPage, current + range)
)
let end = _.range(this.maxPage, Math.max(1, this.maxPage - range))
let allowed = beginning.concat(middle, end)
allowed = _.uniq(allowed)
allowed = _.sortBy(allowed, [(e) => { return e }])
allowed = _.sortBy(allowed, [
e => {
return e
}
])
let final = []
allowed.forEach(p => {
let last = final.slice(-1)[0]
let consecutive = true
if (last === 'skip') {
if (last === "skip") {
consecutive = false
} else {
if (!last) {
......@@ -59,25 +71,25 @@ export default {
if (consecutive) {
final.push(p)
} else {
if (p !== 'skip') {
final.push('skip')
if (p !== "skip") {
final.push("skip")
final.push(p)
}
}
})
return final
},
maxPage: function () {
maxPage: function() {
return Math.ceil(this.total / this.paginateBy)
}
},
methods: {
selectPage: function (page) {
selectPage: function(page) {
if (page > this.maxPage || page < 1) {
return
}
if (this.current !== page) {
this.$emit('page-changed', page)
this.$emit("page-changed", page)
}
}
}
......@@ -87,6 +99,6 @@ export default {
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.ui.pagination.menu .item {
cursor: pointer;
cursor: pointer;
}
</style>
<template>
<div :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]">
<div class="ui inverted segment header-wrapper">
<aside :class="['ui', 'vertical', 'left', 'visible', 'wide', {'collapsed': isCollapsed}, 'sidebar',]">
<header class="ui inverted segment header-wrapper">
<search-bar @search="isCollapsed = false">
<router-link :title="'Funkwhale'" :to="{name: logoUrl}">
<i class="logo bordered inverted orange big icon">
......@@ -12,12 +12,12 @@
:class="['ui', 'basic', 'big', {'inverted': isCollapsed}, 'orange', 'icon', 'collapse', 'button']">
<i class="sidebar icon"></i></span>
</search-bar>
</div>
</header>
<div class="menu-area">
<div class="ui compact fluid two item inverted menu">
<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">
<a class="active item" role="button" @click.prevent.stop="selectedTab = 'library'" data-tab="library"><translate>Browse</translate></a>
<a class="item" role="button" @click.prevent.stop="selectedTab = 'queue'" data-tab="queue">
<translate>Queue</translate>&nbsp;
<template v-if="queue.tracks.length === 0">
<translate>(empty)</translate>
......@@ -29,10 +29,10 @@
</div>
</div>
<div class="tabs">
<div class="ui bottom attached active tab" data-tab="library">
<div class="ui inverted vertical large fluid menu">
<section class="ui bottom attached active tab" data-tab="library" :aria-label="labels.mainMenu">
<nav class="ui inverted vertical large fluid menu" role="navigation" :aria-label="labels.mainMenu">
<div class="item">
<div class="header"><translate>My account</translate></div>
<header class="header"><translate>My account</translate></header>
<div class="menu">
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{name: 'profile', params: {username: $store.state.auth.username}}">
<i class="user icon"></i>
......@@ -61,7 +61,7 @@
</div>
</div>
<div class="item">
<div class="header"><translate>Music</translate></div>
<header class="header"><translate>Music</translate></header>
<div class="menu">
<router-link class="item" :to="{path: '/library'}"><i class="sound icon"></i><translate>Browse library</translate></router-link>
<router-link class="item" v-if="$store.state.auth.authenticated" :to="{path: '/favorites'}"><i class="heart icon"></i><translate>Favorites</translate></router-link>
......@@ -77,7 +77,7 @@
</div>
</div>
<div class="item" v-if="$store.state.auth.availablePermissions['settings']">
<div class="header"><translate>Administration</translate></div>
<header class="header"><translate>Administration</translate></header>
<div class="menu">
<router-link
class="item"
......@@ -91,8 +91,8 @@
</router-link>
</div>
</div>
</div>
</div>
</nav>
</section>
<div v-if="queue.previousQueue " class="ui black icon message">
<i class="history icon"></i>
<div class="content">
......@@ -113,17 +113,21 @@
</div>
</div>
</div>
<div class="ui bottom attached tab" data-tab="queue">
<section class="ui bottom attached tab" data-tab="queue">
<table class="ui compact inverted very basic fixed single line unstackable table">
<draggable v-model="tracks" element="tbody" @update="reorder">
<tr @click="$store.dispatch('queue/currentIndex', index)" v-for="(track, index) in tracks" :key="index" :class="[{'active': index === queue.currentIndex}]">
<tr
@click="$store.dispatch('queue/currentIndex', index)"
v-for="(track, index) in tracks"
:key="index"
:class="[{'active': index === queue.currentIndex}]">
<td class="right aligned">{{ index + 1}}</td>
<td class="center aligned">
<img class="ui mini image" v-if="track.album.cover && track.album.cover.original" :src="$store.getters['instance/absoluteUrl'](track.album.cover.small_square_crop)">
<img class="ui mini image" v-else src="../assets/audio/default-cover.png">
</td>
<td colspan="4">
<button class="title reset ellipsis">
<button class="title reset ellipsis" :aria-label="labels.selectTrack">
<strong>{{ track.title }}</strong><br />
{{ track.artist.name }}
</button>
......@@ -134,7 +138,7 @@
</template>
</td>
<td>
<button @click.stop="cleanTrack(index)" :class="['ui', {'inverted': index != queue.currentIndex}, 'really', 'tiny', 'basic', 'circular', 'icon', 'button']">
<button :title="labels.removeFromQueue" @click.stop="cleanTrack(index)" :class="['ui', {'inverted': index != queue.currentIndex}, 'really', 'tiny', 'basic', 'circular', 'icon', 'button']">
<i class="trash icon"></i>
</button>
</td>
......@@ -150,44 +154,46 @@
<div @click="$store.dispatch('radios/stop')" class="ui basic inverted red button"><translate>Stop radio</translate></div>
</div>
</div>
</div>
</section>
</div>
<player @next="scrollToCurrent" @previous="scrollToCurrent"></player>
</div>
</aside>
</template>
<script>
import {mapState, mapActions} from 'vuex'
import { mapState, mapActions } from "vuex"
import Player from '@/components/audio/Player'
import Logo from '@/components/Logo'
import SearchBar from '@/components/audio/SearchBar'
import backend from '@/audio/backend'
import draggable from 'vuedraggable'
import Player from "@/components/audio/Player"
import Logo from "@/components/Logo"
import SearchBar from "@/components/audio/SearchBar"
import backend from "@/audio/backend"
import draggable from "vuedraggable"
import $ from 'jquery'
import $ from "jquery"
export default {
name: 'sidebar',
name: "sidebar",
components: {
Player,
SearchBar,
Logo,
draggable
},
data () {
data() {
return {
selectedTab: 'library',
selectedTab: "library",
backend: backend,
tracksChangeBuffer: null,
isCollapsed: true,
fetchInterval: null,
fetchInterval: null
}
},
mounted () {
$(this.$el).find('.menu .item').tab()
mounted() {
$(this.$el)
.find(".menu .item")
.tab()
},
destroy () {
destroy() {
if (this.fetchInterval) {
clearInterval(this.fetchInterval)
}
......@@ -197,82 +203,92 @@ export default {
queue: state => state.queue,
url: state => state.route.path
}),
labels () {
let pendingRequests = this.$gettext('Pending import requests')
let pendingFollows = this.$gettext('Pending follow requests')
labels() {
let mainMenu = this.$gettext("Main menu")
let selectTrack = this.$gettext("Play this track")
let pendingRequests = this.$gettext("Pending import requests")
let pendingFollows = this.$gettext("Pending follow requests")
return {
pendingRequests,
pendingFollows
pendingFollows,
mainMenu,
selectTrack
}
},
tracks: {
get () {
get() {
return this.$store.state.queue.tracks
},
set (value) {
set(value) {
this.tracksChangeBuffer = value
}
},
logoUrl () {
logoUrl() {
if (this.$store.state.auth.authenticated) {
return 'library.index'
return "library.index"
} else {
return 'index'
return "index"
}
}
},
methods: {
...mapActions({
cleanTrack: 'queue/cleanTrack'
cleanTrack: "queue/cleanTrack"
}),
reorder: function (event) {
this.$store.commit('queue/reorder', {
tracks: this.tracksChangeBuffer, oldIndex: event.oldIndex, newIndex: event.newIndex})
reorder: function(event) {
this.$store.commit("queue/reorder", {
tracks: this.tracksChangeBuffer,
oldIndex: event.oldIndex,
newIndex: event.newIndex
})
},
scrollToCurrent () {
scrollToCurrent() {
let current = $(this.$el).find('[data-tab="queue"] .active')[0]
if (!current) {
return
}
let container = $(this.$el).find('.tabs')[0]
let container = $(this.$el).find(".tabs")[0]
// Position container at the top line then scroll current into view
container.scrollTop = 0
current.scrollIntoView(true)
// Scroll back nothing if element is at bottom of container else do it
// for half the height of the containers display area
var scrollBack = (container.scrollHeight - container.scrollTop <= container.clientHeight) ? 0 : container.clientHeight / 2
var scrollBack =
container.scrollHeight - container.scrollTop <= container.clientHeight
? 0
: container.clientHeight / 2
container.scrollTop = container.scrollTop - scrollBack
}
},
watch: {
url: function () {
url: function() {
this.isCollapsed = true
},
selectedTab: function (newValue) {
if (newValue === 'queue') {
selectedTab: function(newValue) {
if (newValue === "queue") {
this.scrollToCurrent()
}
},
'$store.state.queue.currentIndex': function () {
if (this.selectedTab !== 'queue') {
"$store.state.queue.currentIndex": function() {
if (this.selectedTab !== "queue") {
this.scrollToCurrent()
}
},