Merge branch 'a11y-p' into 'main'

some a11y tweaks

See merge request PronounsPage/PronounsPage!618
This commit is contained in:
Valentyne Stigloher 2025-05-27 10:32:50 +00:00
commit 2af5e6efaa
32 changed files with 129 additions and 43 deletions

View File

@ -56,6 +56,7 @@ useSeoMeta({
</script>
<template>
<NuxtRouteAnnouncer />
<NuxtPwaManifest />
<NuxtLoadingIndicator color="#C71585" error-color="#dc3545" />
<NuxtLayout>

View File

@ -75,15 +75,15 @@ selectPronounForExample(false);
(<nuxt-link :to="`/${pronoun.canonicalName}`"><Spelling escape :text="pronoun.canonicalName" /></nuxt-link>)
</small>
</ExampleItem>
<Tooltip :text="tooltipText">
<button
v-if="hasDifferentExample"
type="button"
class="btn btn-sm btn-link px-1 py-0"
@click="selectDifferentExample()"
>
<Icon v="random" />
</button>
<Tooltip
v-if="hasDifferentExample"
:text="tooltipText"
tag="button"
type="button"
class="btn btn-sm btn-link px-1 py-0"
@click="selectDifferentExample()"
>
<Icon v="random" />
</Tooltip>
</li>
</template>

View File

@ -4,7 +4,7 @@ import type { Category } from '~/src/classes.ts';
const filter = defineModel<string>();
const filterCategory = defineModel<string>('category');
defineProps<{
const props = defineProps<{
categories?: Category[] | undefined;
submitButton?: boolean;
}>();
@ -17,10 +17,32 @@ const filterInput = useTemplateRef<HTMLInputElement>('filterInput');
const { $translator: translator } = useNuxtApp();
const allCategory: Category = { key: '', text: translator.translate('crud.all'), icon: 'clipboard-list' };
const categoriesWithAllCategory = computed(() => [allCategory, ...(props.categories ?? [])]);
defineExpose({
focus: () => filterInput.value?.focus(),
});
const categoryList = useTemplateRef('categoryList');
const categoryButtonKeydown = (event: KeyboardEvent) => {
if (filterCategory.value === undefined) {
return;
}
const activeIndex = categoriesWithAllCategory.value.map((category) => category.key).indexOf(filterCategory.value);
if (activeIndex === -1) {
return;
}
if ((event.key === 'ArrowUp' || event.key === 'ArrowLeft') &&
activeIndex > 0) {
filterCategory.value = categoriesWithAllCategory.value[activeIndex - 1].key;
(categoryList.value?.children[activeIndex - 1] as HTMLButtonElement | undefined)?.focus();
} else if ((event.key === 'ArrowDown' || event.key === 'ArrowRight') &&
activeIndex < categoriesWithAllCategory.value.length - 1) {
filterCategory.value = categoriesWithAllCategory.value[activeIndex + 1].key;
(categoryList.value?.children[activeIndex + 1] as HTMLButtonElement | undefined)?.focus();
}
};
</script>
<template>
@ -36,7 +58,12 @@ defineExpose({
class="form-control border-primary"
:placeholder="$t('crud.filterLong')"
>
<button v-if="filter" class="btn btn-outline-danger" @click="filter = ''; filterInput?.focus()">
<button
v-if="filter"
class="btn btn-outline-danger"
:title="$t('crud.resetFilter')"
@click="filter = ''; filterInput?.focus()"
>
<Icon v="times" />
</button>
<button
@ -50,17 +77,20 @@ defineExpose({
</div>
<div
v-if="categories && categories.length > 0"
ref="categoryList"
class="d-flex flex-wrap mt-1 border border-primary rounded overflow-hidden"
>
<button
v-for="category of [allCategory, ...categories]"
v-for="category of categoriesWithAllCategory"
:key="category.text"
:class="[
'btn btn-sm btn-wrapped',
filterCategory === category.key ? 'btn-primary' : 'btn-outline-primary',
'flex-grow-1 d-flex justify-content-center align-items-center gap-1 rounded-0',
]"
:tabindex="filterCategory === category.key ? 0 : -1"
@click="filterCategory = category.key"
@keydown="categoryButtonKeydown"
>
<Icon v-if="category.icon" :v="category.icon" />
<Spelling :text="category.text" />

View File

@ -1,12 +1,8 @@
<script setup lang="ts">
import { loadGrammarTableVariantsConverter } from '~/src/data.ts';
import type { Example, ExampleValues } from '~/src/language/examples.ts';
import {
type GrammarTableDefinition,
type Variant,
type SectionDefinition,
expandVariantsForSection,
} from '~/src/language/grammarTables.ts';
import { expandVariantsForSection } from '~/src/language/grammarTables.ts';
import type { GrammarTableDefinition, Variant, SectionDefinition } from '~/src/language/grammarTables.ts';
const props = defineProps<{
grammarTable: GrammarTableDefinition;

View File

@ -286,6 +286,7 @@ const dismissCensus = (): void => {
<template>
<div v-if="config.header" class="mb-lg-4">
<header @mouseleave="hoverItem = null">
<SkipLink />
<div class="d-none d-lg-flex justify-content-between align-items-center flex-row nav-custom btn-group mb-0">
<template v-for="link in links">
<PotentiallyExternalLink

View File

@ -11,7 +11,7 @@
:class="['icon', $attrs.class]"
@error="fallBack"
>
<span v-else :class="[`fa${iconSet}`, `fa-${icon}`, 'fa-fw', hover ? 'fa-hover' : '']" :style="style"></span>
<span v-else aria-hidden="true" :class="[`fa${iconSet}`, `fa-${icon}`, 'fa-fw', hover ? 'fa-hover' : '']" :style></span>
</template>
<script lang="ts">

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import opinions, { type Opinion } from '~/src/opinions.ts';
import opinions from '~/src/opinions.ts';
import type { Opinion } from '~/src/opinions.ts';
const props = withDefaults(defineProps<{
word: string;

View File

@ -11,7 +11,7 @@ defineProps<{
<AdPlaceholder :phkey="['aside-left', null]" class="d-none d-xxl-block" />
</slot>
</aside>
<main :class="[wide ? 'wide' : '']">
<main id="main" :class="[wide ? 'wide' : '']">
<slot></slot>
</main>
<aside v-if="!wide" class="aside-right">

View File

@ -20,7 +20,11 @@ defineProps<{
</p>
<SimplePronounList :pronouns="Array.isArray(pronouns) ? pronouns : Object.values(pronouns)" />
</li>
<nuxt-link :to="{ name: 'pronouns' }" class="list-group-item list-group-item-action text-center">
<nuxt-link
:to="{ name: 'pronouns' }"
class="list-group-item list-group-item-action text-center"
:title="$t('home.pronouns')"
>
<Icon v="ellipsis-h-alt" />
</nuxt-link>
</ul>

20
components/SkipLink.vue Normal file
View File

@ -0,0 +1,20 @@
<script setup lang="ts">
const router = useRouter();
router.afterEach(async (to, from) => {
if (to.path !== from.path) {
await nextTick();
// reset the focus to the start of the document
// so that the skip link is the first element in tab order after navigation
document.body.tabIndex = 0;
document.body.focus();
document.body.tabIndex = -1;
}
});
</script>
<template>
<a ref="link" href="#main" class="sr-only sr-only-focusable position-fixed top-0 z-1">
<T>home.skipToContent</T>
</a>
</template>

View File

@ -55,7 +55,7 @@ defineExpose({
<template>
<section ref="section" class="table-responsive scroll-mt-7">
<div class="container">
<div class="container" role="table">
<nav v-if="pages > 1" class="d-flex justify-content-center p-2">
<ul class="pagination pagination-sm justify-content-center mb-0">
<li
@ -78,7 +78,7 @@ defineExpose({
<strong>{{ data.length }}</strong>
</div>
</div>
<div class="row-header p-2 d-grid gap-2 border-top">
<div class="row-header p-2 d-grid gap-2 border-top" role="row">
<slot name="header"></slot>
</div>
<template v-if="data.length">
@ -86,6 +86,7 @@ defineExpose({
v-for="el in dataPage"
:key="el.id"
:class="['row-content p-2 d-grid gap-2 border-top', marked?.(el) ? 'marked' : '']"
role="row"
>
<slot name="row" :el="el"></slot>
</div>

View File

@ -72,6 +72,7 @@ const setSpelling = (spelling: string) => {
class="d-inline-block"
:end="end"
menu-class="locale-dropdown shadow"
:title="$t('links.languageVersions')"
>
<template #toggle>
<Icon v="language" />

View File

@ -119,7 +119,11 @@ const nounConventionGroup = computed(() => {
<li class="list-group-item">
<NounsConventionsIndexGroup :noun-convention-group />
</li>
<nuxt-link :to="{ name: 'nouns' }" class="list-group-item list-group-item-action text-center">
<nuxt-link
:to="{ name: 'nouns' }"
class="list-group-item list-group-item-action text-center"
:title="$t('nouns.conventions.header')"
>
<Icon v="ellipsis-h-alt" />
</nuxt-link>
</ul>

View File

@ -127,7 +127,12 @@ defineExpose({ loadNouns });
fixed
>
<template #header>
<div v-for="gender in availableGenders(config)" :key="gender" class="d-none d-md-block bold">
<div
v-for="gender in availableGenders(config)"
:key="gender"
class="d-none d-md-block bold"
role="columnheader"
>
<NounsGenderLabel :gender="gender" />
</div>
</template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { type Gender, iconNamesByGender, longIdentifierByGender } from '~/src/nouns.ts';
import { iconNamesByGender, longIdentifierByGender } from '~/src/nouns.ts';
import type { Gender } from '~/src/nouns.ts';
const props = defineProps<{
gender: Gender;

View File

@ -32,6 +32,7 @@ const numerus = computed(() => {
v-for="plural in numerus"
:key="plural ? 'plural' : 'singular'"
:style="{ gridArea: `${gender}${plural ? 'Pl' : ''}` }"
role="cell"
>
<NounsItem :noun="noun" :gender="gender" :plural="plural" />

View File

@ -17,7 +17,7 @@ export default () => {
const filter: Ref<Filter> = ref({ text: '', category: '', moderation: undefined });
onBeforeRouteUpdate((to) => {
if (to.hash) {
if (to.hash && !document.querySelector(to.hash)) {
return { query: { filter: to.hash.substring(1).replace(/=$/, '') }, replace: true };
}
});

View File

@ -141,9 +141,10 @@ export default withNuxt(
})
.override('nuxt/import/rules', {
rules: {
'import/consistent-type-specifier-style': ['warn', 'prefer-top-level'],
'import/extensions': ['error', 'ignorePackages'],
'import/no-useless-path-segments': 'error',
'import/order': ['error', {
'import/no-useless-path-segments': 'warn',
'import/order': ['warn', {
'newlines-between': 'always',
'alphabetize': { order: 'asc', orderImportKind: 'desc' },
}],

View File

@ -7,6 +7,7 @@ home:
header: 'Pronouns'
headerLong: 'List of pronouns'
welcome: 'Welcome to pronouns.page!'
skipToContent: 'Skip to content'
intro: >
We're creating a source of information about nonbinary and gender neutral language.
why: 'What''s the deal with pronouns?'
@ -213,6 +214,7 @@ nouns:
apply: 'Apply template'
overwrite: 'Are you sure you want to apply this template? This will replace previous inputs'
conventions:
header: 'Noun conventions'
intro: 'Use for me'
masculine: 'masculine'
masculineShort: 'masc.'
@ -958,6 +960,7 @@ crud:
add: 'Add'
filter: 'Search…'
filterLong: 'Search…'
resetFilter: 'Reset filter'
search: 'Search…'
all: 'All'
author: 'Added by'

View File

@ -1,7 +1,7 @@
<template>
<div>
<Separator icon="book-alt" />
<h3>Substantivkonventionen</h3>
<h3><T>nouns.conventions.header</T></h3>
<NounsConventionsIndex />
<Separator icon="book-open" />
<slot></slot>

View File

@ -7,6 +7,7 @@ home:
header: 'Pronomen'
headerLong: 'Liste von Pronomen'
welcome: 'Willkommen zu Pronomen.net!'
skipToContent: 'zum Inhalt springen'
intro: >
Wir sind eine Informationsquelle über nichtbinäre und geschlechtsneutrale Sprache.
why: 'Warum sind Pronomen wichtig?'
@ -233,6 +234,7 @@ nouns:
apply: 'Vorlage anwenden'
overwrite: 'Bist du sicher, dass du diese Vorlage anwenden möchtest? Sie ersetzt die vorherigen Eingaben'
conventions:
header: 'Substantivkonventionen'
intro: 'Verwende für mich'
masculine: 'Maskulin'
masculineShort: 'Mask.'
@ -1076,6 +1078,7 @@ crud:
add: 'Hinzufügen'
filter: 'Filter'
filterLong: 'Liste filtern...'
resetFilter: 'Filter zurücksetzen'
search: 'Suchen…'
all: 'Alle'
author: 'Hinzugefügt von'

View File

@ -7,6 +7,7 @@ home:
header: 'Pronouns'
headerLong: 'List of pronouns'
welcome: 'Welcome to pronouns.page!'
skipToContent: 'Skip to content'
intro: >
We're creating a source of information about nonbinary and gender neutral language.
why: 'What''s the deal with pronouns?'
@ -1174,6 +1175,7 @@ crud:
add: 'Add'
filter: 'Search…'
filterLong: 'Search…'
resetFilter: 'Reset filter'
search: 'Search…'
all: 'All'
author: 'Added by'

View File

@ -4,7 +4,8 @@ import { useNuxtApp } from 'nuxt/app';
import NounsNav from './NounsNav.vue';
import useConfig from '~/composables/useConfig.ts';
import { Noun, type Source, SourceLibrary } from '~/src/classes.ts';
import { Noun, SourceLibrary } from '~/src/classes.ts';
import type { Source } from '~/src/classes.ts';
const { $translator: translator } = useNuxtApp();
useSimpleHead({

View File

@ -2,8 +2,10 @@
import { useDebounce, useLocalStorage } from '@vueuse/core';
import marked from 'marked';
import { extractMetadata, type Post } from '~/src/blog/metadata.ts';
import parseMarkdown, { type MarkdownInfo } from '~/src/parseMarkdown.ts';
import { extractMetadata } from '~/src/blog/metadata.ts';
import type { Post } from '~/src/blog/metadata.ts';
import parseMarkdown from '~/src/parseMarkdown.ts';
import type { MarkdownInfo } from '~/src/parseMarkdown.ts';
const content = useLocalStorage(
'admin/blog/editor',

View File

@ -12,8 +12,8 @@ import {
MONTHS as months,
AREAS as areas,
TRANSFER_METHODS as transferMethods,
type TimesheetData, type Timesheet,
} from '~/src/timesheets.ts';
import type { TimesheetData, Timesheet } from '~/src/timesheets.ts';
const { $translator: translator } = useNuxtApp();
const dialogue = useDialogue();

View File

@ -1,9 +1,11 @@
<script setup lang="ts">
import { DateTime, type DurationInput } from 'luxon';
import { DateTime } from 'luxon';
import type { DurationInput } from 'luxon';
import { useNuxtApp } from 'nuxt/app';
import useSimpleHead from '~/composables/useSimpleHead.ts';
import { min, max, MONTHS, PERIODS, type TimesheetData } from '~/src/timesheets.ts';
import { min, max, MONTHS, PERIODS } from '~/src/timesheets.ts';
import type { TimesheetData } from '~/src/timesheets.ts';
function* range(start: number, end: number) {
for (let i = start; i <= end; i++) {

View File

@ -12,7 +12,8 @@ import { getUrlForLocale } from '~/src/domain.ts';
import { buildFlags } from '~/src/flags.ts';
import { sleep } from '~/src/helpers.ts';
import opinions from '~/src/opinions.ts';
import { applyProfileVisibilityRules, type UserWithProfiles, type Profile } from '~/src/profile.ts';
import { applyProfileVisibilityRules } from '~/src/profile.ts';
import type { UserWithProfiles, Profile } from '~/src/profile.ts';
definePageMeta({
translatedPaths: (config) => {

View File

@ -22,7 +22,8 @@ import forbidden from '~/src/forbidden.ts';
import { clearLinkedText, buildImageUrl } from '~/src/helpers.ts';
import { genders, gendersWithNumerus } from '~/src/nouns.ts';
import parseMarkdown from '~/src/parseMarkdown.ts';
import { normaliseQuery, type SearchDocument, validateQuery } from '~/src/search.ts';
import { normaliseQuery, validateQuery } from '~/src/search.ts';
import type { SearchDocument } from '~/src/search.ts';
interface SearchKind {
kind: SearchDocument['kind'];

View File

@ -1,4 +1,5 @@
import { type Config, type ConfigWithEnabled, isEnabled } from '../locale/config.ts';
import { isEnabled } from '../locale/config.ts';
import type { Config, ConfigWithEnabled } from '../locale/config.ts';
import type { PronounData, PronounGroupData } from '../locale/data.ts';
import { Pronoun, PronounGroup } from './classes.ts';

View File

@ -1,5 +1,6 @@
import type { ExampleValues } from '~/src/language/examples.ts';
import { type MorphemeValue, toMorphemeValue } from '~/src/language/morphemes.ts';
import { toMorphemeValue } from '~/src/language/morphemes.ts';
import type { MorphemeValue } from '~/src/language/morphemes.ts';
export type GrammarTablesDefinition = (GrammarTableDefinition | string)[]
| { simple: (GrammarTableDefinition | string)[]; comprehensive: (GrammarTableDefinition | string)[] };

View File

@ -8,7 +8,8 @@ import { loadSuml, loadTsv } from '~/server/loader.ts';
import { normaliseKey } from '~/src/buildPronoun.ts';
import { Example } from '~/src/language/examples.ts';
import type { VariantsFromBaseConverter } from '~/src/language/grammarTables.ts';
import { availableGenders, gendersWithNumerus, type NounConventions } from '~/src/nouns.ts';
import { availableGenders, gendersWithNumerus } from '~/src/nouns.ts';
import type { NounConventions } from '~/src/nouns.ts';
function toHaveValidMorphemes(actual: string, morphemes: string[]): SyncExpectationResult {
const containedMorphemes = Example.parse(actual).parts

View File

@ -5,7 +5,8 @@ import { promisify } from 'util';
import { chromium } from '@playwright/test';
import type { Browser, Page } from '@playwright/test';
import { execa, type ResultPromise } from 'execa';
import { execa } from 'execa';
import type { ResultPromise } from 'execa';
import tkKill from 'tree-kill';
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
import { createRouter, createMemoryHistory } from 'vue-router';