Various accessibility improvements

* Add announcement element with `aria-live`
* Add skip to main content element

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2021-10-10 16:24:12 +02:00
parent 6113836e29
commit eba3c70c9b
62 changed files with 687 additions and 175 deletions

View File

@@ -1,28 +1,6 @@
<template>
<div>
<div class="hero intro is-small is-primary">
<div class="hero-body">
<div class="container">
<h1 class="title">{{ $t("About Mobilizon") }}</h1>
<p>
{{
$t(
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising."
)
}}
</p>
<b-button
icon-left="open-in-new"
size="is-large"
type="is-secondary"
tag="a"
href="https://joinmobilizon.org"
>{{ $t("Learn more") }}</b-button
>
</div>
</div>
</div>
<main class="container">
<section class="container">
<div class="columns">
<div class="column is-one-quarter-desktop">
<aside class="menu">
@@ -62,8 +40,29 @@
<router-view />
</div>
</div>
</main>
</section>
<div class="hero intro is-small is-secondary">
<div class="hero-body">
<div class="container">
<h1 class="title">{{ $t("Powered by Mobilizon") }}</h1>
<p>
{{
$t(
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising."
)
}}
</p>
<b-button
icon-left="open-in-new"
size="is-large"
type="is-primary"
tag="a"
href="https://joinmobilizon.org"
>{{ $t("Learn more") }}</b-button
>
</div>
</div>
</div>
<div
class="hero register is-primary is-medium"
v-if="!currentUser || !currentUser.id"

View File

@@ -3,7 +3,7 @@
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
<h2 class="title">{{ config.name }}</h2>
<h1 class="title">{{ config.name }}</h1>
<p>{{ config.description }}</p>
</div>
</div>
@@ -24,7 +24,7 @@
</i18n>
</div>
<div class="column contact">
<h4>{{ $t("Contact") }}</h4>
<p class="has-text-weight-bold">{{ $t("Contact") }}</p>
<instance-contact-link
v-if="config && config.contact"
:contact="config.contact"
@@ -32,13 +32,13 @@
<p v-else>{{ $t("No information") }}</p>
</div>
</section>
<hr />
<hr role="presentation" />
<section class="long-description content">
<div v-html="config.longDescription" />
</section>
<hr />
<hr role="presentation" />
<section class="config">
<h3 class="subtitle">{{ $t("Instance configuration") }}</h3>
<h2 class="subtitle">{{ $t("Instance configuration") }}</h2>
<table class="table is-fullwidth">
<tr>
<td>{{ $t("Instance languages") }}</td>
@@ -168,7 +168,7 @@ section {
}
&.hero {
h2.title {
h1.title {
margin: auto;
}
}
@@ -195,7 +195,7 @@ section {
}
}
.contact {
h4 {
h3 {
font-weight: bold;
}
p {

View File

@@ -307,6 +307,14 @@ const MEMBERSHIPS_PER_PAGE = 10;
ActorCard,
EmptyContent,
},
metaInfo() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { person } = this;
return {
title: person ? person.name || usernameWithDomain(person) : "",
};
},
})
export default class AdminProfile extends Vue {
@Prop({ required: true }) id!: string;

View File

@@ -96,6 +96,14 @@ import { IPerson } from "../../types/actor";
},
},
},
metaInfo() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { user } = this;
return {
title: user.email,
};
},
})
export default class AdminUserProfile extends Vue {
@Prop({ required: true }) id!: string;

View File

@@ -87,6 +87,11 @@ import RouteName from "../../router/name";
},
},
},
metaInfo() {
return {
title: this.$t("Follows") as string,
};
},
})
export default class Follows extends Vue {
RouteName = RouteName;

View File

@@ -55,7 +55,7 @@
</b-field>
<b-field :label="$t('Text')">
<editor v-model="discussion.text" />
<editor v-model="discussion.text" :aria-label="$t('Comment body')" />
</b-field>
<button class="button is-primary" type="submit">

View File

@@ -125,7 +125,7 @@
>
<form @submit.prevent="reply" v-if="!error">
<b-field :label="$t('Text')">
<editor v-model="newComment" />
<editor v-model="newComment" :aria-label="$t('Comment body')" />
</b-field>
<b-button
native-type="submit"

View File

@@ -84,7 +84,10 @@
<div class="field">
<label class="label">{{ $t("Description") }}</label>
<editor v-model="event.description" />
<editor
v-model="event.description"
:aria-label="$t('Event description body')"
/>
</div>
<b-field :label="$t('Website / URL')" label-for="website-url">

View File

@@ -245,12 +245,14 @@
aria-role="listitem"
v-if="canManageEvent || event.draft"
@click="openDeleteEventModalWrapper"
@keyup.enter="openDeleteEventModalWrapper"
>
{{ $t("Delete") }}
<b-icon icon="delete" />
</b-dropdown-item>
<hr
role="presentation"
class="dropdown-divider"
aria-role="menuitem"
v-if="canManageEvent || event.draft"
@@ -259,6 +261,7 @@
aria-role="listitem"
v-if="!event.draft"
@click="triggerShare()"
@keyup.enter="triggerShare()"
>
<span>
{{ $t("Share this event") }}
@@ -268,6 +271,7 @@
<b-dropdown-item
aria-role="listitem"
@click="downloadIcsEvent()"
@keyup.enter="downloadIcsEvent()"
v-if="!event.draft"
>
<span>
@@ -279,6 +283,7 @@
aria-role="listitem"
v-if="ableToReport"
@click="isReportModalActive = true"
@keyup.enter="isReportModalActive = true"
>
<span>
{{ $t("Report") }}
@@ -379,6 +384,7 @@
class="button"
ref="cancelButton"
@click="isJoinModalActive = false"
@keyup.enter="isJoinModalActive = false"
>
{{ $t("Cancel") }}
</button>
@@ -390,6 +396,11 @@
? joinEventWithConfirmation(identity)
: joinEvent(identity)
"
@keyup.enter="
event.joinOptions === EventJoinOptions.RESTRICTED
? joinEventWithConfirmation(identity)
: joinEvent(identity)
"
>
{{ $t("Confirm my particpation") }}
</button>
@@ -436,6 +447,7 @@
class="button"
ref="cancelButton"
@click="isJoinConfirmationModalActive = false"
@keyup.enter="isJoinConfirmationModalActive = false"
>{{ $t("Cancel") }}
</b-button>
<b-button type="is-primary" native-type="submit">

View File

@@ -62,13 +62,17 @@
</template>
<b-dropdown-item
has-link
v-for="format in exportFormats"
:key="format"
@click="exportParticipants(format)"
aria-role="listitem"
@click="exportParticipants(format)"
@keyup.enter="exportParticipants(format)"
>
<b-icon :icon="formatToIcon(format)"></b-icon>
{{ format }}
<button class="dropdown-button">
<b-icon :icon="formatToIcon(format)"></b-icon>
{{ format }}
</button>
</b-dropdown-item>
</b-dropdown>
</div>
@@ -566,4 +570,21 @@ nav.breadcrumb {
text-decoration: none;
}
}
button.dropdown-button {
&:hover {
background-color: #f5f5f5;
color: #0a0a0a;
}
width: 100%;
display: flex;
flex: 1;
background: white;
border: none;
cursor: pointer;
color: #4a4a4a;
font-size: 0.875rem;
line-height: 1.5;
padding: 0.375rem 1rem;
}
</style>

View File

@@ -203,6 +203,7 @@
</span>
</b-dropdown-item>
<hr
role="presentation"
class="dropdown-divider"
v-if="isCurrentActorAGroupMember"
/>
@@ -224,7 +225,7 @@
{{ $t("ICS/WebCal Feed") }}
</a>
</b-dropdown-item>
<hr class="dropdown-divider" />
<hr role="presentation" class="dropdown-divider" />
<b-dropdown-item
v-if="ableToReport"
aria-role="menuitem"

View File

@@ -41,7 +41,11 @@
<b-input v-model="editableGroup.name" id="group-settings-name" />
</b-field>
<b-field :label="$t('Group short description')">
<editor mode="basic" v-model="editableGroup.summary" :maxSize="500"
<editor
mode="basic"
v-model="editableGroup.summary"
:maxSize="500"
:aria-label="$t('Group description body')"
/></b-field>
<b-field :label="$t('Avatar')">
<picture-upload

View File

@@ -39,6 +39,11 @@ import SettingMenuItem from "../../components/Settings/SettingMenuItem.vue";
@Component({
components: { SettingMenuSection, SettingMenuItem },
metaInfo() {
return {
title: this.$t("Group settings") as string,
};
},
})
export default class Settings extends mixins(GroupMixin) {
RouteName = RouteName;

View File

@@ -241,6 +241,7 @@
</span>
</section>
<hr
role="presentation"
class="home-separator"
v-if="canShowMyUpcomingEvents && canShowLastWeekEvents"
/>
@@ -259,6 +260,7 @@
</div>
</section>
<hr
role="presentation"
class="home-separator"
v-if="canShowLastWeekEvents && canShowCloseEvents"
/>
@@ -297,6 +299,7 @@
</div>
</section>
<hr
role="presentation"
class="home-separator"
v-if="
canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents

View File

@@ -395,6 +395,11 @@ import { Paginate } from "@/types/paginate";
},
},
},
metaInfo() {
return {
title: this.$t("Moderation logs") as string,
};
},
})
export default class ReportList extends Vue {
actionLogs?: Paginate<IActionLog> = { total: 0, elements: [] };

View File

@@ -111,6 +111,11 @@ const REPORT_PAGE_LIMIT = 10;
pollInterval: 120000, // 2 minutes
},
},
metaInfo() {
return {
title: this.$t("Reports") as string,
};
},
})
export default class ReportList extends Vue {
reports?: Paginate<IReport> = { elements: [], total: 0 };

View File

@@ -72,7 +72,7 @@
<div class="field">
<label class="label">{{ $t("Post") }}</label>
<p v-if="errors.body" class="help is-danger">{{ errors.body }}</p>
<editor v-model="editablePost.body" />
<editor v-model="editablePost.body" :aria-label="$t('Post body')" />
</div>
<subtitle>{{ $t("Who can view this post") }}</subtitle>
<fieldset>

View File

@@ -127,11 +127,13 @@ const POSTS_PAGE_LIMIT = 10;
PostElementItem,
},
metaInfo() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { group } = this;
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: this.$t("My groups") as string,
titleTemplate: "%s | Mobilizon",
title: this.$t("{group} posts", {
group: group.name || usernameWithDomain(group),
}) as string,
};
},
})

View File

@@ -58,6 +58,7 @@
{{ $t("New link") }}
</b-dropdown-item>
<hr
role="presentation"
class="dropdown-divider"
v-if="config.resourceProviders.length"
/>

View File

@@ -18,12 +18,16 @@
<div class="setting-title">
<h2>{{ $t("Browser notifications") }}</h2>
</div>
<b-button v-if="subscribed" @click="unsubscribeToWebPush()">{{
$t("Unsubscribe to browser push notifications")
}}</b-button>
<b-button
v-if="subscribed"
@click="unsubscribeToWebPush()"
@keyup.enter="unsubscribeToWebPush()"
>{{ $t("Unsubscribe to browser push notifications") }}</b-button
>
<b-button
icon-left="rss"
@click="subscribeToWebPush"
@keyup.enter="subscribeToWebPush"
v-else-if="canShowWebPush && webPushEnabled"
>{{ $t("Activate browser push notifications") }}</b-button
>
@@ -247,6 +251,9 @@
@click="
(e) => copyURL(e, tokenToURL(feedToken.token, 'atom'), 'atom')
"
@keyup.enter="
(e) => copyURL(e, tokenToURL(feedToken.token, 'atom'), 'atom')
"
:href="tokenToURL(feedToken.token, 'atom')"
target="_blank"
>{{ $t("RSS/Atom Feed") }}</b-button
@@ -264,6 +271,9 @@
@click="
(e) => copyURL(e, tokenToURL(feedToken.token, 'ics'), 'ics')
"
@keyup.enter="
(e) => copyURL(e, tokenToURL(feedToken.token, 'ics'), 'ics')
"
icon-left="calendar-sync"
:href="tokenToURL(feedToken.token, 'ics')"
target="_blank"
@@ -274,6 +284,7 @@
icon-left="refresh"
type="is-text"
@click="openRegenerateFeedTokensConfirmation"
@keyup.enter="openRegenerateFeedTokensConfirmation"
>{{ $t("Regenerate new links") }}</b-button
>
</div>
@@ -283,6 +294,7 @@
icon-left="refresh"
type="is-text"
@click="generateFeedTokens"
@keyup.enter="generateFeedTokens"
>{{ $t("Create new links") }}</b-button
>
</div>
@@ -333,7 +345,7 @@ type NotificationType = { label: string; subtypes: NotificationSubType[] };
},
metaInfo() {
return {
title: this.$t("Notifications") as string,
title: this.$t("Notification settings") as string,
};
},
})

View File

@@ -61,7 +61,7 @@
<b-message v-else type="is-danger">{{
$t("Unable to detect timezone.")
}}</b-message>
<hr />
<hr role="presentation" />
<b-field grouped>
<b-field
:label="$t('City or region')"
@@ -95,6 +95,7 @@
<b-button
:disabled="address == undefined"
@click="resetArea"
@keyup.enter="resetArea"
class="reset-area"
icon-left="close"
:aria-label="$t('Reset')"

View File

@@ -55,6 +55,14 @@ import RouteName from "../../router/name";
},
},
},
metaInfo() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { todo } = this;
return {
title: todo.title,
};
},
})
export default class Todo extends Vue {
@Prop({ type: String, required: true }) todoId!: string;

View File

@@ -69,6 +69,14 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
},
currentActor: CURRENT_ACTOR_CLIENT,
},
metaInfo() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { todoList } = this;
return {
title: todoList.title,
};
},
})
export default class TodoList extends Vue {
@Prop({ type: String, required: true }) id!: string;

View File

@@ -82,6 +82,16 @@ import RouteName from "../../router/name";
components: {
CompactTodo,
},
metaInfo() {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { group } = this;
return {
title: this.$t("{group}'s todolists", {
group: group.name || usernameWithDomain(group),
}) as string,
};
},
})
export default class TodoLists extends Vue {
@Prop({ type: String, required: true }) preferredUsername!: string;

View File

@@ -24,7 +24,13 @@ import { VALIDATE_EMAIL } from "../../graphql/user";
import RouteName from "../../router/name";
import { ICurrentUser } from "../../types/current-user.model";
@Component
@Component({
metaInfo() {
return {
title: this.$t("Validating email") as string,
};
},
})
export default class Validate extends Vue {
@Prop({ type: String, required: true }) token!: string;

View File

@@ -50,7 +50,13 @@ import { saveUserData } from "../../utils/auth";
import { ILogin } from "../../types/login.model";
import RouteName from "../../router/name";
@Component
@Component({
metaInfo() {
return {
title: this.$t("Password reset") as string,
};
},
})
export default class PasswordReset extends Vue {
@Prop({ type: String, required: true }) token!: string;

View File

@@ -9,7 +9,13 @@ import RouteName from "../../router/name";
import { saveUserData, changeIdentity } from "../../utils/auth";
import { IUser } from "../../types/current-user.model";
@Component
@Component({
metaInfo() {
return {
title: this.$t("Redirecting to Mobilizon") as string,
};
},
})
export default class ProviderValidate extends Vue {
async mounted(): Promise<void> {
const accessToken = this.getValueFromMeta("auth-access-token");

View File

@@ -59,7 +59,7 @@
<router-link class="out" :to="{ name: RouteName.ABOUT }">{{
$t("Learn more")
}}</router-link>
<hr />
<hr role="presentation" />
<div class="content">
<subtitle>{{
$t("About {instance}", { instance: config.name })
@@ -170,7 +170,7 @@
>
</p>
<hr />
<hr role="presentation" />
<div
class="control"
v-if="config && config.auth.oauthProviders.length > 0"

View File

@@ -56,7 +56,13 @@ import {
import { RESEND_CONFIRMATION_EMAIL } from "../../graphql/auth";
import RouteName from "../../router/name";
@Component
@Component({
metaInfo() {
return {
title: this.$t("Resend confirmation email") as string,
};
},
})
export default class ResendConfirmation extends Vue {
@Prop({ type: String, required: false, default: "" }) email!: string;

View File

@@ -71,7 +71,13 @@ import {
import { SEND_RESET_PASSWORD } from "../../graphql/auth";
import RouteName from "../../router/name";
@Component
@Component({
metaInfo() {
return {
title: this.$t("Reset password") as string,
};
},
})
export default class SendPasswordReset extends Vue {
@Prop({ type: String, required: false, default: "" }) email!: string;

View File

@@ -66,6 +66,11 @@ import { IConfig } from "../../types/config.model";
apollo: {
config: TIMEZONES,
},
metaInfo() {
return {
title: this.$t("First steps") as string,
};
},
})
export default class SettingsOnboard extends Vue {
@Prop({ required: false, default: 1, type: Number }) step!: number;

View File

@@ -29,7 +29,13 @@ import RouteName from "../../router/name";
import { saveUserData, saveTokenData, changeIdentity } from "../../utils/auth";
import { ILogin } from "../../types/login.model";
@Component
@Component({
metaInfo() {
return {
title: this.$t("Validating account") as string,
};
},
})
export default class Validate extends Vue {
@Prop({ type: String, required: true }) token!: string;