Commit c0904ca8 authored by Eliot Berriot's avatar Eliot Berriot 💬

Merge branch '612-semantic-elements' into 'develop'

Fix #612: Improved accessibility by using main/section/nav tags and aria-labels…

Closes #612

See merge request funkwhale/funkwhale!471
parents 9005ebbd 29171853
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>
This diff is collapsed.
This diff is collapsed.
<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 small text container">
<h2><translate>Log in to your Funkwhale account</translate></h2>
<form class="ui form" @submit.prevent="submit()">
......@@ -43,39 +43,39 @@
</button>
</form>
</div>
</div>
</div>
</section>
</main>
</template>
<script>
import PasswordInput from '@/components/forms/PasswordInput'
import PasswordInput from "@/components/forms/PasswordInput"
export default {
props: {
next: {type: String, default: '/'}
next: { type: String, default: "/" }
},
components: {
PasswordInput
},
data () {
data() {
return {
// We need to initialize the component with any
// properties that will be used in it
credentials: {
username: '',
password: ''
username: "",
password: ""
},
error: '',
error: "",
isLoading: false
}
},
mounted () {
mounted() {
this.$refs.username.focus()
},
computed: {
labels () {
let usernamePlaceholder = this.$gettext('Enter your username or email')
let title = this.$gettext('Log In')
labels() {
let usernamePlaceholder = this.$gettext("Enter your username or email")
let title = this.$gettext("Log In")
return {
usernamePlaceholder,
title
......@@ -83,30 +83,31 @@ export default {
}
},
methods: {
submit () {
submit() {
var self = this
self.isLoading = true
this.error = ''
this.error = ""
var credentials = {
username: this.credentials.username,
password: this.credentials.password
}
this.$store.dispatch('auth/login', {
credentials,
next: '/library',
onError: error => {
if (error.response.status === 400) {
self.error = 'invalid_credentials'
} else {
self.error = 'unknown_error'
this.$store
.dispatch("auth/login", {
credentials,
next: "/library",
onError: error => {
if (error.response.status === 400) {
self.error = "invalid_credentials"
} else {
self.error = "unknown_error"
}
}
}
}).then(e => {
self.isLoading = false
})
})
.then(e => {
self.isLoading = false
})
}
}
}
</script>
......
<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 small text container">
<h2>
<translate>Are you sure you want to log out?</translate>
......@@ -8,16 +8,16 @@
<p v-translate="{username: $store.state.auth.username}">You are currently logged in as %{ username }</p>
<button class="ui button" @click="$store.dispatch('auth/logout')"><translate>Yes, log me out!</translate></button>
</div>
</div>
</div>
</section>
</main>
</template>
<script>
export default {
computed: {
labels () {
labels() {
return {
title: this.$gettext('Log Out')
title: this.$gettext("Log Out")
}
}
}
......
<template>
<div class="main pusher" v-title="labels.usernameProfile">
<main class="main pusher" v-title="labels.usernameProfile">
<div v-if="isLoading" class="ui vertical segment">
<div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
</div>
......@@ -25,36 +25,37 @@
</a>
</div>
</template>
</div>
</main>
</template>
<script>
import {mapState} from 'vuex'
import { mapState } from "vuex"
const dateFormat = require('dateformat')
const dateFormat = require("dateformat")
export default {
props: ['username'],
created () {
this.$store.dispatch('auth/fetchProfile')
props: ["username"],
created() {
this.$store.dispatch("auth/fetchProfile")
},
computed: {
...mapState({
profile: state => state.auth.profile
}),
labels () {
let msg = this.$gettext('%{ username }\'s profile')
let usernameProfile = this.$gettextInterpolate(msg, {username: this.username})
labels() {
let msg = this.$gettext("%{ username }'s profile")
let usernameProfile = this.$gettextInterpolate(msg, {
username: this.username
})
return {
usernameProfile
}
},
signupDate () {
signupDate() {
let d = new Date(this.profile.date_joined)
return dateFormat(d, 'longDate')
return dateFormat(d, "longDate")
},
isLoading () {
isLoading() {
return !this.profile
}
}
......
<template>
<div class="main pusher" v-title="labels.title">
<main class="main pusher" v-title="labels.title">
<div class="ui vertical stripe segment">
<div class="ui small text container">
<section class="ui small text container">
<h2 class="ui header">
<translate>Account settings</translate>
</h2>
......@@ -28,9 +28,9 @@
<translate>Update settings</translate>
</button>
</form>
</div>
</section>
<div class="ui hidden divider"></div>
<div class="ui small text container">
<section class="ui small text container">
<h2 class="ui header">
<translate>Avatar</translate>
</h2>
......@@ -61,9 +61,9 @@
</div>
</div>
</div>
</div>
</section>
<div class="ui hidden divider"></div>
<div class="ui small text container">
<section class="ui small text container">
<h2 class="ui header">
<translate>Change my password</translate>
</h2>
......@@ -107,18 +107,18 @@
</form>
<div class="ui hidden divider" />
<subsonic-token-form />
</div>
</section>
</div>
</div>
</main>
</template>
<script>
import $ from 'jquery'
import axios from 'axios'
import logger from '@/logging'
import PasswordInput from '@/components/forms/PasswordInput'
import SubsonicTokenForm from '@/components/auth/SubsonicTokenForm'
import TranslationsMixin from '@/components/mixins/Translations'
import $ from "jquery"
import axios from "axios"
import logger from "@/logging"
import PasswordInput from "@/components/forms/PasswordInput"
import SubsonicTokenForm from "@/components/auth/SubsonicTokenForm"
import TranslationsMixin from "@/components/mixins/Translations"
export default {
mixins: [TranslationsMixin],
......@@ -126,14 +126,14 @@ export default {
PasswordInput,
SubsonicTokenForm
},
data () {
data() {
let d = {
// We need to initialize the component with any
// properties that will be used in it
old_password: '',
new_password: '',
old_password: "",
new_password: "",
currentAvatar: this.$store.state.auth.profile.avatar,
passwordError: '',
passwordError: "",
isLoading: false,
isLoadingAvatar: false,
avatarErrors: [],
......@@ -141,12 +141,12 @@ export default {
settings: {
success: false,
errors: [],
order: ['privacy_level'],
order: ["privacy_level"],
fields: {
'privacy_level': {
type: 'dropdown',
privacy_level: {
type: "dropdown",
initial: this.$store.state.auth.profile.privacy_level,
choices: ['me', 'instance']
choices: ["me", "instance"]
}
}
}
......@@ -157,108 +157,120 @@ export default {
})
return d
},
mounted () {
$('select.dropdown').dropdown()
mounted() {
$("select.dropdown").dropdown()
},
methods: {
submitSettings () {
submitSettings() {
this.settings.success = false
this.settings.errors = []
let self = this
let payload = this.settingsValues
let url = `users/users/${this.$store.state.auth.username}/`
return axios.patch(url, payload).then(response => {
logger.default.info('Updated settings successfully')
self.settings.success = true
return axios.get('users/users/me/').then((response) => {
self.$store.dispatch('auth/updateProfile', response.data)
})
}, error => {
logger.default.error('Error while updating settings')
self.isLoading = false
self.settings.errors = error.backendErrors
})
return axios.patch(url, payload).then(
response => {
logger.default.info("Updated settings successfully")
self.settings.success = true
return axios.get("users/users/me/").then(response => {
self.$store.dispatch("auth/updateProfile", response.data)
})
},
error => {
logger.default.error("Error while updating settings")
self.isLoading = false
self.settings.errors = error.backendErrors
}
)
},
submitAvatar () {
submitAvatar() {
this.isLoadingAvatar = true
this.avatarErrors = []
let self = this
this.avatar = this.$refs.avatar.files[0]
let formData = new FormData()
formData.append('avatar', this.avatar)
axios.patch(
`users/users/${this.$store.state.auth.username}/`,
formData,
{
formData.append("avatar", this.avatar)
axios
.patch(`users/users/${this.$store.state.auth.username}/`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
"Content-Type": "multipart/form-data"
}
}
).then(response => {
this.isLoadingAvatar = false
self